Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2022-07-11 20:07:33 +10:00
commit 75f8d8b391
90 changed files with 10346 additions and 9196 deletions

31
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# Runs on releases
name: Publish release notes
on:
release:
types: [published]
jobs:
tweet:
runs-on: ubuntu-latest
steps:
- uses: ethomson/send-tweet-action@v1
with:
status: "InvenTree release ${{ github.event.release.tag_name }} is out now! Release notes: ${{ github.event.release.html_url }} #opensource #inventree"
consumer-key: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
consumer-secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
reddit:
runs-on: ubuntu-latest
steps:
- uses: bluwy/release-for-reddit-action@v1
with:
username: ${{ secrets.REDDIT_USERNAME }}
password: ${{ secrets.REDDIT_PASSWORD }}
app-id: ${{ secrets.REDDIT_APP_ID }}
app-secret: ${{ secrets.REDDIT_APP_SECRET }}
subreddit: InvenTree
title: "InvenTree version ${{ github.event.release.tag_name }} released"
comment: "${{ github.event.release.body }}"

View File

@ -133,8 +133,12 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
if expected_code is not None:
if response.status_code != expected_code:
print(f"Unexpected response at '{url}':")
print(response.data)
print(f"Unexpected response at '{url}': status code = {response.status_code}")
if hasattr(response, 'data'):
print(response.data)
else:
print(f"(response object {type(response)} has no 'data' attribute")
self.assertEqual(response.status_code, expected_code)

View File

@ -2,11 +2,15 @@
# InvenTree API version
INVENTREE_API_VERSION = 61
INVENTREE_API_VERSION = 64
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v64 -> 2022-07-08 : https://github.com/inventree/InvenTree/pull/3310
- Annotate 'on_order' quantity to BOM list API
- Allow BOM List API endpoint to be filtered by "on_order" parameter
v63 -> 2022-07-06 : https://github.com/inventree/InvenTree/pull/3301
- Allow BOM List API endpoint to be filtered by "available_stock" paramater

View File

@ -63,10 +63,15 @@ class InvenTreeModelMoneyField(ModelMoneyField):
# Set a minimum value validator
validators = kwargs.get('validators', [])
allow_negative = kwargs.pop('allow_negative', False)
# If no validators are provided, add some "standard" ones
if len(validators) == 0:
validators.append(
MinMoneyValidator(0),
)
if not allow_negative:
validators.append(
MinMoneyValidator(0),
)
kwargs['validators'] = validators

View File

@ -0,0 +1,156 @@
"""Custom string formatting functions and helpers"""
import re
import string
from django.utils.translation import gettext_lazy as _
def parse_format_string(fmt_string: str) -> dict:
"""Extract formatting information from the provided format string.
Returns a dict object which contains structured information about the format groups
"""
groups = string.Formatter().parse(fmt_string)
info = {}
for group in groups:
# Skip any group which does not have a named value
if not group[1]:
continue
info[group[1]] = {
'format': group[1],
'prefix': group[0],
}
return info
def construct_format_regex(fmt_string: str) -> str:
r"""Construct a regular expression based on a provided format string
This function turns a python format string into a regular expression,
which can be used for two purposes:
- Ensure that a particular string matches the specified format
- Extract named variables from a matching string
This function also provides support for wildcard characters:
- '?' provides single character matching; is converted to a '.' (period) for regex
- '#' provides single digit matching; is converted to '\d'
Args:
fmt_string: A typical format string e.g. "PO-???-{ref:04d}"
Returns:
str: A regular expression pattern e.g. ^PO\-...\-(?P<ref>.*)$
Raises:
ValueError: Format string is invalid
"""
pattern = "^"
for group in string.Formatter().parse(fmt_string):
prefix = group[0] # Prefix (literal text appearing before this group)
name = group[1] # Name of this format variable
format = group[2] # Format specifier e.g :04d
rep = [
'+', '-', '.',
'{', '}', '(', ')',
'^', '$', '~', '!', '@', ':', ';', '|', '\'', '"',
]
# Escape any special regex characters
for ch in rep:
prefix = prefix.replace(ch, '\\' + ch)
# Replace ? with single-character match
prefix = prefix.replace('?', '.')
# Replace # with single-digit match
prefix = prefix.replace('#', r'\d')
pattern += prefix
# Add a named capture group for the format entry
if name:
# Check if integer values are requried
if format.endswith('d'):
chr = '\d'
else:
chr = '.'
# Specify width
# TODO: Introspect required width
w = '+'
pattern += f"(?P<{name}>{chr}{w})"
pattern += "$"
return pattern
def validate_string(value: str, fmt_string: str) -> str:
"""Validate that the provided string matches the specified format.
Args:
value: The string to be tested e.g. 'SO-1234-ABC',
fmt_string: The required format e.g. 'SO-{ref}-???',
Returns:
bool: True if the value matches the required format, else False
Raises:
ValueError: The provided format string is invalid
"""
pattern = construct_format_regex(fmt_string)
result = re.match(pattern, value)
return result is not None
def extract_named_group(name: str, value: str, fmt_string: str) -> str:
"""Extract a named value from the provided string, given the provided format string
Args:
name: Name of group to extract e.g. 'ref'
value: Raw string e.g. 'PO-ABC-1234'
fmt_string: Format pattern e.g. 'PO-???-{ref}
Returns:
str: String value of the named group
Raises:
ValueError: format string is incorrectly specified, or provided value does not match format string
NameError: named value does not exist in the format string
IndexError: named value could not be found in the provided entry
"""
info = parse_format_string(fmt_string)
if name not in info.keys():
raise NameError(_(f"Value '{name}' does not appear in pattern format"))
# Construct a regular expression for matching against the provided format string
# Note: This will raise a ValueError if 'fmt_string' is incorrectly specified
pattern = construct_format_regex(fmt_string)
# Run the regex matcher against the raw string
result = re.match(pattern, value)
if not result:
raise ValueError(_("Provided value does not match required pattern: ") + fmt_string)
# And return the value we are interested in
# Note: This will raise an IndexError if the named group was not matched
return result.group(name)

View File

@ -15,7 +15,6 @@ from allauth.account.forms import SignupForm, set_form_field_order
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth_2fa.adapter import OTPAdapter
from allauth_2fa.forms import TOTPDeviceRemoveForm
from allauth_2fa.utils import user_has_valid_totp_device
from crispy_forms.bootstrap import (AppendedText, PrependedAppendedText,
PrependedText)
@ -270,36 +269,3 @@ class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
# Otherwise defer to the original allauth adapter.
return super().login(request, user)
# Temporary fix for django-allauth-2fa # TODO remove
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
class CustomTOTPDeviceRemoveForm(TOTPDeviceRemoveForm):
"""Custom Form to ensure a token is provided before removing MFA"""
# User must input a valid token so 2FA can be removed
token = forms.CharField(
label=_('Token'),
)
def __init__(self, user, **kwargs):
"""Add token field."""
super().__init__(user, **kwargs)
self.fields['token'].widget.attrs.update(
{
'autofocus': 'autofocus',
'autocomplete': 'off',
}
)
def clean_token(self):
"""Ensure at least one valid token is provided."""
# Ensure that the user has provided a valid token
token = self.cleaned_data.get('token')
# Verify that the user has provided a valid token
for device in self.user.totpdevice_set.filter(confirmed=True):
if device.verify_token(token):
return token
raise forms.ValidationError(_("The entered token is not valid"))

View File

@ -3,6 +3,7 @@
import logging
import os
import re
from datetime import datetime
from django.conf import settings
from django.contrib.auth import get_user_model
@ -19,7 +20,9 @@ from error_report.models import Error
from mptt.exceptions import InvalidMove
from mptt.models import MPTTModel, TreeForeignKey
import InvenTree.format
import InvenTree.helpers
from common.models import InvenTreeSetting
from InvenTree.fields import InvenTreeURLField
from InvenTree.validators import validate_tree_name
@ -96,9 +99,6 @@ class DataImportMixin(object):
class ReferenceIndexingMixin(models.Model):
"""A mixin for keeping track of numerical copies of the "reference" field.
!!DANGER!! always add `ReferenceIndexingSerializerMixin`to all your models serializers to
ensure the reference field is not too big
Here, we attempt to convert a "reference" field value (char) to an integer,
for performing fast natural sorting.
@ -112,24 +112,216 @@ class ReferenceIndexingMixin(models.Model):
- Otherwise, we store zero
"""
# Name of the global setting which defines the required reference pattern for this model
REFERENCE_PATTERN_SETTING = None
@classmethod
def get_reference_pattern(cls):
"""Returns the reference pattern associated with this model.
This is defined by a global setting object, specified by the REFERENCE_PATTERN_SETTING attribute
"""
# By default, we return an empty string
if cls.REFERENCE_PATTERN_SETTING is None:
return ''
return InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
@classmethod
def get_reference_context(cls):
"""Generate context data for generating the 'reference' field for this class.
- Returns a python dict object which contains the context data for formatting the reference string.
- The default implementation provides some default context information
"""
return {
'ref': cls.get_next_reference(),
'date': datetime.now(),
}
@classmethod
def get_most_recent_item(cls):
"""Return the item which is 'most recent'
In practice, this means the item with the highest reference value
"""
query = cls.objects.all().order_by('-reference_int', '-pk')
if query.exists():
return query.first()
else:
return None
@classmethod
def get_next_reference(cls):
"""Return the next available reference value for this particular class."""
# Find the "most recent" item
latest = cls.get_most_recent_item()
if not latest:
# No existing items
return 1
reference = latest.reference.strip
try:
reference = InvenTree.format.extract_named_group('ref', reference, cls.get_reference_pattern())
except Exception:
# If reference cannot be extracted using the pattern, try just the integer value
reference = str(latest.reference_int)
# Attempt to perform 'intelligent' incrementing of the reference field
incremented = InvenTree.helpers.increment(reference)
try:
incremented = int(incremented)
except ValueError:
pass
return incremented
@classmethod
def generate_reference(cls):
"""Generate the next 'reference' field based on specified pattern"""
fmt = cls.get_reference_pattern()
ctx = cls.get_reference_context()
reference = None
attempts = set()
while reference is None:
try:
ref = fmt.format(**ctx)
if ref in attempts:
# We are stuck in a loop!
reference = ref
break
else:
attempts.add(ref)
if cls.objects.filter(reference=ref).exists():
# Handle case where we have duplicated an existing reference
ctx['ref'] = InvenTree.helpers.increment(ctx['ref'])
else:
# We have found an 'unused' reference
reference = ref
break
except Exception:
# If anything goes wrong, return the most recent reference
recent = cls.get_most_recent_item()
if recent:
reference = recent.reference
else:
reference = ""
return reference
@classmethod
def validate_reference_pattern(cls, pattern):
"""Ensure that the provided pattern is valid"""
ctx = cls.get_reference_context()
try:
info = InvenTree.format.parse_format_string(pattern)
except Exception:
raise ValidationError({
"value": _("Improperly formatted pattern"),
})
# Check that only 'allowed' keys are provided
for key in info.keys():
if key not in ctx.keys():
raise ValidationError({
"value": _("Unknown format key specified") + f": '{key}'"
})
# Check that the 'ref' variable is specified
if 'ref' not in info.keys():
raise ValidationError({
'value': _("Missing required format key") + ": 'ref'"
})
@classmethod
def validate_reference_field(cls, value):
"""Check that the provided 'reference' value matches the requisite pattern"""
pattern = cls.get_reference_pattern()
value = str(value).strip()
if len(value) == 0:
raise ValidationError(_("Reference field cannot be empty"))
# An 'empty' pattern means no further validation is required
if not pattern:
return
if not InvenTree.format.validate_string(value, pattern):
raise ValidationError(_("Reference must match required pattern") + ": " + pattern)
# Check that the reference field can be rebuild
cls.rebuild_reference_field(value, validate=True)
class Meta:
"""Metaclass options. Abstract ensures no database table is created."""
abstract = True
def rebuild_reference_field(self):
"""Extract integer out of reference for sorting."""
reference = getattr(self, 'reference', '')
self.reference_int = extract_int(reference)
@classmethod
def rebuild_reference_field(cls, reference, validate=False):
"""Extract integer out of reference for sorting.
If the 'integer' portion is buried somewhere 'within' the reference,
we can first try to extract it using the pattern.
Example:
reference - BO-123-ABC
pattern - BO-{ref}-???
extracted - 123
If we cannot extract using the pattern for some reason, fallback to the entire reference
"""
try:
# Extract named group based on provided pattern
reference = InvenTree.format.extract_named_group('ref', reference, cls.get_reference_pattern())
except Exception:
pass
reference_int = extract_int(reference)
if validate:
if reference_int > models.BigIntegerField.MAX_BIGINT:
raise ValidationError({
"reference": _("Reference number is too large")
})
return reference_int
reference_int = models.BigIntegerField(default=0)
def extract_int(reference, clip=0x7fffffff):
"""Extract integer out of reference."""
def extract_int(reference, clip=0x7fffffff, allow_negative=False):
"""Extract an integer out of reference."""
# Default value if we cannot convert to an integer
ref_int = 0
reference = str(reference).strip()
# Ignore empty string
if len(reference) == 0:
return 0
# Look at the start of the string - can it be "integerized"?
result = re.match(r"^(\d+)", reference)
@ -139,6 +331,16 @@ def extract_int(reference, clip=0x7fffffff):
ref_int = int(ref)
except Exception:
ref_int = 0
else:
# Look at the "end" of the string
result = re.search(r'(\d+)$', reference)
if result and len(result.groups()) == 1:
ref = result.groups()[0]
try:
ref_int = int(ref)
except Exception:
ref_int = 0
# Ensure that the returned values are within the range that can be stored in an IntegerField
# Note: This will result in large values being "clipped"
@ -148,6 +350,9 @@ def extract_int(reference, clip=0x7fffffff):
elif ref_int < -clip:
ref_int = -clip
if not allow_negative and ref_int < 0:
ref_int = abs(ref_int)
return ref_int

View File

@ -7,7 +7,6 @@ from decimal import Decimal
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
import tablib
@ -20,8 +19,6 @@ from rest_framework.fields import empty
from rest_framework.serializers import DecimalField
from rest_framework.utils import model_meta
from .models import extract_int
class InvenTreeMoneySerializer(MoneyField):
"""Custom serializer for 'MoneyField', which ensures that passed values are numerically valid.
@ -211,16 +208,6 @@ class UserSerializer(InvenTreeModelSerializer):
]
class ReferenceIndexingSerializerMixin():
"""This serializer mixin ensures the the reference is not to big / small for the BigIntegerField."""
def validate_reference(self, value):
"""Ensures the reference is not to big / small for the BigIntegerField."""
if extract_int(value) > models.BigIntegerField.MAX_BIGINT:
raise serializers.ValidationError('reference is to to big')
return value
class InvenTreeAttachmentSerializerField(serializers.FileField):
"""Override the DRF native FileField serializer, to remove the leading server path.

File diff suppressed because one or more lines are too long

View File

@ -40,8 +40,7 @@ function attachClipboard(selector, containerselector, textElement) {
}
// create Clipboard
// eslint-disable-next-line no-unused-vars
var cis = new ClipboardJS(selector, {
new ClipboardJS(selector, {
text: text,
container: containerselector
});
@ -110,6 +109,9 @@ function inventreeDocReady() {
$('#launch-about').click(function() {
launchModalForm(`/about/`, {
no_post: true,
after_render: function() {
attachClipboard('.clip-btn', 'modal-form');
}
});
});

View File

@ -17,6 +17,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import Rate, convert_money
from djmoney.money import Money
import InvenTree.format
import InvenTree.tasks
from common.models import InvenTreeSetting
from common.settings import currency_codes
@ -60,6 +61,137 @@ class ValidatorTest(TestCase):
validate_overage("aaaa")
class FormatTest(TestCase):
"""Unit tests for custom string formatting functionality"""
def test_parse(self):
"""Tests for the 'parse_format_string' function"""
# Extract data from a valid format string
fmt = "PO-{abc:02f}-{ref:04d}-{date}-???"
info = InvenTree.format.parse_format_string(fmt)
self.assertIn('abc', info)
self.assertIn('ref', info)
self.assertIn('date', info)
# Try with invalid strings
for fmt in [
'PO-{{xyz}',
'PO-{xyz}}',
'PO-{xyz}-{',
]:
with self.assertRaises(ValueError):
InvenTree.format.parse_format_string(fmt)
def test_create_regex(self):
"""Test function for creating a regex from a format string"""
tests = {
"PO-123-{ref:04f}": r"^PO\-123\-(?P<ref>.+)$",
"{PO}-???-{ref}-{date}-22": r"^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$",
"ABC-123-###-{ref}": r"^ABC\-123\-\d\d\d\-(?P<ref>.+)$",
"ABC-123": r"^ABC\-123$",
}
for fmt, reg in tests.items():
self.assertEqual(InvenTree.format.construct_format_regex(fmt), reg)
def test_validate_format(self):
"""Test that string validation works as expected"""
# These tests should pass
for value, pattern in {
"ABC-hello-123": "???-{q}-###",
"BO-1234": "BO-{ref}",
"111.222.fred.china": "???.###.{name}.{place}",
"PO-1234": "PO-{ref:04d}"
}.items():
self.assertTrue(InvenTree.format.validate_string(value, pattern))
# These tests should fail
for value, pattern in {
"ABC-hello-123": "###-{q}-???",
"BO-1234": "BO.{ref}",
"BO-####": "BO-{pattern}-{next}",
"BO-123d": "BO-{ref:04d}"
}.items():
self.assertFalse(InvenTree.format.validate_string(value, pattern))
def test_extract_value(self):
"""Test that we can extract named values based on a format string"""
# Simple tests based on a straight-forward format string
fmt = "PO-###-{ref:04d}"
tests = {
"123": "PO-123-123",
"456": "PO-123-456",
"789": "PO-123-789",
}
for k, v in tests.items():
self.assertEqual(InvenTree.format.extract_named_group('ref', v, fmt), k)
# However these ones should fail
tests = {
'abc': 'PO-123-abc',
'xyz': 'PO-123-xyz',
}
for v in tests.values():
with self.assertRaises(ValueError):
InvenTree.format.extract_named_group('ref', v, fmt)
# More complex tests
fmt = "PO-{date}-{test}-???-{ref}-###"
val = "PO-2022-02-01-hello-ABC-12345-222"
data = {
'date': '2022-02-01',
'test': 'hello',
'ref': '12345',
}
for k, v in data.items():
self.assertEqual(InvenTree.format.extract_named_group(k, val, fmt), v)
# Test for error conditions
# Raises a ValueError as the format string is bad
with self.assertRaises(ValueError):
InvenTree.format.extract_named_group(
"test",
"PO-1234-5",
"PO-{test}-{"
)
# Raises a NameError as the named group does not exist in the format string
with self.assertRaises(NameError):
InvenTree.format.extract_named_group(
"missing",
"PO-12345",
"PO-{test}",
)
# Raises a ValueError as the value does not match the format string
with self.assertRaises(ValueError):
InvenTree.format.extract_named_group(
"test",
"PO-1234",
"PO-{test}-1234",
)
with self.assertRaises(ValueError):
InvenTree.format.extract_named_group(
"test",
"PO-ABC-xyz",
"PO-###-{test}",
)
class TestHelpers(TestCase):
"""Tests for InvenTree helper functions."""

View File

@ -57,17 +57,6 @@ def validate_part_ipn(value):
raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))
def validate_build_order_reference(value):
"""Validate the 'reference' field of a BuildOrder."""
pattern = common.models.InvenTreeSetting.get_setting('BUILDORDER_REFERENCE_REGEX')
if pattern:
match = re.search(pattern, value)
if match is None:
raise ValidationError(_('Reference must match pattern {pattern}').format(pattern=pattern))
def validate_purchase_order_reference(value):
"""Validate the 'reference' field of a PurchaseOrder."""
pattern = common.models.InvenTreeSetting.get_setting('PURCHASEORDER_REFERENCE_REGEX')

View File

@ -37,7 +37,7 @@ from common.settings import currency_code_default, currency_codes
from part.models import PartCategory
from users.models import RuleSet, check_user_role
from .forms import CustomTOTPDeviceRemoveForm, EditUserForm, SetPasswordForm
from .forms import EditUserForm, SetPasswordForm
def auth_request(request):
@ -764,10 +764,8 @@ class NotificationsView(TemplateView):
template_name = "InvenTree/notifications/notifications.html"
# Temporary fix for django-allauth-2fa # TODO remove
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
# Custom 2FA removal form to allow custom redirect URL
class CustomTwoFactorRemove(TwoFactorRemove):
"""Use custom form."""
form_class = CustomTOTPDeviceRemoveForm
"""Specify custom URL redirect."""
success_url = reverse_lazy("settings")

View File

@ -5,7 +5,7 @@
fields:
part: 100 # Build against part 100 "Bob"
batch: 'B1'
reference: "0001"
reference: "BO-0001"
title: 'Building 7 parts'
quantity: 7
notes: 'Some simple notes'
@ -21,7 +21,7 @@
pk: 2
fields:
part: 50
reference: "0002"
reference: "BO-0002"
title: 'Making things'
batch: 'B2'
status: 40 # COMPLETE
@ -37,7 +37,7 @@
pk: 3
fields:
part: 50
reference: "0003"
reference: "BO-003"
title: 'Making things'
batch: 'B2'
status: 40 # COMPLETE
@ -53,7 +53,7 @@
pk: 4
fields:
part: 50
reference: "0004"
reference: "BO-4"
title: 'Making things'
batch: 'B4'
status: 40 # COMPLETE
@ -69,7 +69,7 @@
pk: 5
fields:
part: 25
reference: "0005"
reference: "BO-0005"
title: "Building some Widgets"
batch: "B10"
status: 40 # Complete

View File

@ -1,6 +1,6 @@
# Generated by Django 3.0.7 on 2020-10-19 13:02
import InvenTree.validators
import build.validators
from django.db import migrations, models
@ -18,6 +18,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(help_text='Build Order Reference', max_length=64, unique=True, validators=[InvenTree.validators.validate_build_order_reference], verbose_name='Reference'),
field=models.CharField(help_text='Build Order Reference', max_length=64, unique=True, validators=[build.validators.validate_build_order_reference], verbose_name='Reference'),
),
]

View File

@ -1,6 +1,6 @@
# Generated by Django 3.2.4 on 2021-07-08 14:14
import InvenTree.validators
import build.validators
import build.models
from django.db import migrations, models
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='build',
name='reference',
field=models.CharField(default=build.models.get_next_build_number, help_text='Build Order Reference', max_length=64, unique=True, validators=[InvenTree.validators.validate_build_order_reference], verbose_name='Reference'),
field=models.CharField(default=build.validators.generate_next_build_reference, help_text='Build Order Reference', max_length=64, unique=True, validators=[build.validators.validate_build_order_reference], verbose_name='Reference'),
),
]

View File

@ -0,0 +1,69 @@
# Generated by Django 3.2.14 on 2022-07-07 11:01
from django.db import migrations
def update_build_reference(apps, schema_editor):
"""Update the build order reference.
Ref: https://github.com/inventree/InvenTree/pull/3267
Performs the following steps:
- Extract existing 'prefix' value
- Generate a build order pattern based on the prefix value
- Update any existing build order references with the specified prefix
"""
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
try:
prefix = InvenTreeSetting.objects.get(key='BUILDORDER_REFERENCE_PREFIX').value
except Exception:
prefix = 'BO-'
# Construct a reference pattern
pattern = prefix + '{ref:04d}'
# Create or update the BuildOrder.reference pattern
try:
setting = InvenTreeSetting.objects.get(key='BUILDORDER_REFERENCE_PATTERN')
setting.value = pattern
setting.save()
except InvenTreeSetting.DoesNotExist:
setting = InvenTreeSetting.objects.create(
key='BUILDORDER_REFERENCE_PATTERN',
value=pattern,
)
# Update any existing build order references with the prefix
Build = apps.get_model('build', 'build')
n = 0
for build in Build.objects.all():
if not build.reference.startswith(prefix):
build.reference = prefix + build.reference
build.save()
n += 1
if n > 0:
print(f"Updated reference field for {n} BuildOrder objects")
def nupdate_build_reference(apps, schema_editor):
"""Reverse migration code. Does nothing."""
pass
class Migration(migrations.Migration):
dependencies = [
('build', '0035_alter_build_notes'),
]
operations = [
migrations.RunPython(
update_build_reference,
reverse_code=nupdate_build_reference,
)
]

View File

@ -22,12 +22,14 @@ from mptt.exceptions import InvalidMove
from rest_framework import serializers
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode, notify_responsible
from InvenTree.helpers import increment, normalize, MakeBarcode, notify_responsible
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
from InvenTree.validators import validate_build_order_reference
from build.validators import generate_next_build_reference, validate_build_order_reference
import InvenTree.fields
import InvenTree.helpers
import InvenTree.ready
import InvenTree.tasks
from plugin.events import trigger_event
@ -38,32 +40,6 @@ from stock import models as StockModels
from users import models as UserModels
def get_next_build_number():
"""Returns the next available BuildOrder reference number."""
if Build.objects.count() == 0:
return '0001'
build = Build.objects.exclude(reference=None).last()
attempts = {build.reference}
reference = build.reference
while 1:
reference = increment(reference)
if reference in attempts:
# Escape infinite recursion
return reference
if Build.objects.filter(reference=reference).exists():
attempts.add(reference)
else:
break
return reference
class Build(MPTTModel, ReferenceIndexingMixin):
"""A Build object organises the creation of new StockItem objects from other existing StockItem objects.
@ -89,6 +65,9 @@ class Build(MPTTModel, ReferenceIndexingMixin):
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
@staticmethod
def get_api_url():
"""Return the API URL associated with the BuildOrder model"""
@ -106,7 +85,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
def api_defaults(cls, request):
"""Return default values for this model when issuing an API OPTIONS request."""
defaults = {
'reference': get_next_build_number(),
'reference': generate_next_build_reference(),
}
if request and request.user:
@ -116,7 +95,8 @@ class Build(MPTTModel, ReferenceIndexingMixin):
def save(self, *args, **kwargs):
"""Custom save method for the BuildOrder model"""
self.rebuild_reference_field()
self.validate_reference_field(self.reference)
self.reference_int = self.rebuild_reference_field(self.reference)
try:
super().save(*args, **kwargs)
@ -172,9 +152,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
def __str__(self):
"""String representation of a BuildOrder"""
prefix = getSetting("BUILDORDER_REFERENCE_PREFIX")
return f"{prefix}{self.reference}"
return self.reference
def get_absolute_url(self):
"""Return the web URL associated with this BuildOrder"""
@ -186,9 +164,9 @@ class Build(MPTTModel, ReferenceIndexingMixin):
blank=False,
help_text=_('Build Order Reference'),
verbose_name=_('Reference'),
default=get_next_build_number,
default=generate_next_build_reference,
validators=[
validate_build_order_reference
validate_build_order_reference,
]
)
@ -199,7 +177,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
help_text=_('Brief description of the build')
)
# TODO - Perhaps delete the build "tree"
parent = TreeForeignKey(
'self',
on_delete=models.SET_NULL,
@ -1092,6 +1069,10 @@ class Build(MPTTModel, ReferenceIndexingMixin):
@receiver(post_save, sender=Build, dispatch_uid='build_post_save_log')
def after_save_build(sender, instance: Build, created: bool, **kwargs):
"""Callback function to be executed after a Build instance is saved."""
# Escape if we are importing data
if InvenTree.ready.isImportingData() or not InvenTree.ready.canAppAccessDatabase(allow_test=True):
return
from . import tasks as build_tasks
if created:

View File

@ -11,7 +11,7 @@ from rest_framework import serializers
from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import ReferenceIndexingSerializerMixin, UserSerializer
from InvenTree.serializers import UserSerializer
import InvenTree.helpers
from InvenTree.helpers import extract_serial_numbers
@ -28,7 +28,7 @@ from users.serializers import OwnerSerializer
from .models import Build, BuildItem, BuildOrderAttachment
class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
class BuildSerializer(InvenTreeModelSerializer):
"""Serializes a Build object."""
url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -74,6 +74,16 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
if part_detail is not True:
self.fields.pop('part_detail')
reference = serializers.CharField(required=True)
def validate_reference(self, reference):
"""Custom validation for the Build reference field"""
# Ensure the reference matches the required pattern
Build.validate_reference_field(reference)
return reference
class Meta:
"""Serializer metaclass"""
model = Build

View File

@ -748,6 +748,7 @@ class BuildListTest(BuildAPITest):
Build.objects.create(
part=part,
reference="BO-0006",
quantity=10,
title='Just some thing',
status=BuildStatus.PRODUCTION,
@ -773,20 +774,23 @@ class BuildListTest(BuildAPITest):
Build.objects.create(
part=part,
quantity=10,
reference=f"build-000{i}",
reference=f"BO-{i + 10}",
title=f"Sub build {i}",
parent=parent
)
# And some sub-sub builds
for sub_build in Build.objects.filter(parent=parent):
for ii, sub_build in enumerate(Build.objects.filter(parent=parent)):
for i in range(3):
x = ii * 10 + i + 50
Build.objects.create(
part=part,
reference=f"{sub_build.reference}-00{i}-sub",
reference=f"BO-{x}",
title=f"{sub_build.reference}-00{i}-sub",
quantity=40,
title=f"sub sub build {i}",
parent=sub_build
)

View File

@ -12,7 +12,7 @@ from InvenTree import status_codes as status
import common.models
import build.tasks
from build.models import Build, BuildItem, get_next_build_number
from build.models import Build, BuildItem, generate_next_build_reference
from part.models import Part, BomItem, BomItemSubstitute
from stock.models import StockItem
from users.models import Owner
@ -88,7 +88,7 @@ class BuildTestBase(TestCase):
quantity=2
)
ref = get_next_build_number()
ref = generate_next_build_reference()
# Create a "Build" object to make 10x objects
self.build = Build.objects.create(
@ -133,20 +133,97 @@ class BuildTest(BuildTestBase):
def test_ref_int(self):
"""Test the "integer reference" field used for natural sorting"""
for ii in range(10):
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'BO-{ref}-???', change_user=None)
refs = {
'BO-123-456': 123,
'BO-456-123': 456,
'BO-999-ABC': 999,
'BO-123ABC-ABC': 123,
'BO-ABC123-ABC': 123,
}
for ref, ref_int in refs.items():
build = Build(
reference=f"{ii}_abcde",
reference=ref,
quantity=1,
part=self.assembly,
title="Making some parts"
title='Making some parts',
)
self.assertEqual(build.reference_int, 0)
build.save()
self.assertEqual(build.reference_int, ref_int)
# After saving, the integer reference should have been updated
self.assertEqual(build.reference_int, ii)
def test_ref_validation(self):
"""Test that the reference field validation works as expected"""
# Default reference pattern = 'BO-{ref:04d}
# These patterns should fail
for ref in [
'BO-1234x',
'BO1234',
'OB-1234',
'BO--1234'
]:
with self.assertRaises(ValidationError):
Build.objects.create(
part=self.assembly,
quantity=10,
reference=ref,
title='Invalid reference',
)
for ref in [
'BO-1234',
'BO-9999',
'BO-123'
]:
Build.objects.create(
part=self.assembly,
quantity=10,
reference=ref,
title='Valid reference',
)
# Try a new validator pattern
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', '{ref}-BO', change_user=None)
for ref in [
'1234-BO',
'9999-BO'
]:
Build.objects.create(
part=self.assembly,
quantity=10,
reference=ref,
title='Valid reference',
)
def test_next_ref(self):
"""Test that the next reference is automatically generated"""
common.models.InvenTreeSetting.set_setting('BUILDORDER_REFERENCE_PATTERN', 'XYZ-{ref:06d}', change_user=None)
build = Build.objects.create(
part=self.assembly,
quantity=5,
reference='XYZ-987',
title='Some thing',
)
self.assertEqual(build.reference_int, 987)
# Now create one *without* specifying the reference
build = Build.objects.create(
part=self.assembly,
quantity=1,
title='Some new title',
)
self.assertEqual(build.reference, 'XYZ-000988')
self.assertEqual(build.reference_int, 988)
def test_init(self):
"""Perform some basic tests before we start the ball rolling"""
@ -404,7 +481,7 @@ class BuildTest(BuildTestBase):
"""Test that a notification is sent when a new build is created"""
Build.objects.create(
reference='IIIII',
reference='BO-9999',
title='Some new build',
part=self.assembly,
quantity=5,

View File

@ -104,3 +104,57 @@ class TestReferenceMigration(MigratorTestCase):
# Check that the build reference is properly assigned
for build in Build.objects.all():
self.assertEqual(str(build.reference), str(build.pk))
class TestReferencePatternMigration(MigratorTestCase):
"""Unit test for data migration which converts reference to new format.
Ref: https://github.com/inventree/InvenTree/pull/3267
"""
migrate_from = ('build', '0019_auto_20201019_1302')
migrate_to = ('build', helpers.getNewestMigrationFile('build'))
def prepare(self):
"""Create some initial data prior to migration"""
Setting = self.old_state.apps.get_model('common', 'inventreesetting')
# Create a custom existing prefix so we can confirm the operation is working
Setting.objects.create(
key='BUILDORDER_REFERENCE_PREFIX',
value='BuildOrder-',
)
Part = self.old_state.apps.get_model('part', 'part')
assembly = Part.objects.create(
name='Assy 1',
description='An assembly',
level=0, lft=0, rght=0, tree_id=0,
)
Build = self.old_state.apps.get_model('build', 'build')
for idx in range(1, 11):
Build.objects.create(
part=assembly,
title=f"Build {idx}",
quantity=idx,
reference=f"{idx + 100}",
level=0, lft=0, rght=0, tree_id=0,
)
def test_reference_migration(self):
"""Test that the reference fields have been correctly updated"""
Build = self.new_state.apps.get_model('build', 'build')
for build in Build.objects.all():
self.assertTrue(build.reference.startswith('BuildOrder-'))
Setting = self.new_state.apps.get_model('common', 'inventreesetting')
pattern = Setting.objects.get(key='BUILDORDER_REFERENCE_PATTERN')
self.assertEqual(pattern.value, 'BuildOrder-{ref:04d}')

View File

@ -35,7 +35,7 @@ class BuildTestSimple(InvenTreeTestCase):
self.assertEqual(b.batch, 'B2')
self.assertEqual(b.quantity, 21)
self.assertEqual(str(b), 'BO0002')
self.assertEqual(str(b), 'BO-0002')
def test_url(self):
"""Test URL lookup"""
@ -75,11 +75,6 @@ class BuildTestSimple(InvenTreeTestCase):
self.assertEqual(b1.is_active, True)
self.assertEqual(b2.is_active, False)
def test_required_parts(self):
"""Test set of required BOM items for the build"""
# TODO: Generate BOM for test part
...
def test_cancel_build(self):
"""Test build cancellation function."""
build = Build.objects.get(id=1)

View File

@ -0,0 +1,25 @@
"""Validation methods for the build app"""
def generate_next_build_reference():
"""Generate the next available BuildOrder reference"""
from build.models import Build
return Build.generate_reference()
def validate_build_order_reference_pattern(pattern):
"""Validate the BuildOrder reference 'pattern' setting"""
from build.models import Build
Build.validate_reference_pattern(pattern)
def validate_build_order_reference(value):
"""Validate that the BuildOrder reference field matches the required pattern"""
from build.models import Build
Build.validate_reference_field(value)

View File

@ -36,10 +36,12 @@ from djmoney.contrib.exchange.models import convert_money
from djmoney.settings import CURRENCY_CHOICES
from rest_framework.exceptions import PermissionDenied
import build.validators
import InvenTree.fields
import InvenTree.helpers
import InvenTree.ready
import InvenTree.validators
import order.validators
logger = logging.getLogger('inventree')
@ -341,6 +343,10 @@ class BaseInvenTreeSetting(models.Model):
# Unless otherwise specified, attempt to create the setting
create = kwargs.get('create', True)
# Prevent creation of new settings objects when importing data
if InvenTree.ready.isImportingData() or not InvenTree.ready.canAppAccessDatabase(allow_test=True):
create = False
if create:
# Attempt to create a new settings object
setting = cls(
@ -850,6 +856,13 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': False,
},
'INVENTREE_REQUIRE_CONFIRM': {
'name': _('Require confirm'),
'description': _('Require explicit user confirmation for certain action.'),
'validator': bool,
'default': True,
},
'BARCODE_ENABLE': {
'name': _('Barcode Support'),
'description': _('Enable barcode scanner support'),
@ -1132,21 +1145,18 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
},
'BUILDORDER_REFERENCE_PREFIX': {
'name': _('Build Order Reference Prefix'),
'description': _('Prefix value for build order reference'),
'default': 'BO',
'BUILDORDER_REFERENCE_PATTERN': {
'name': _('Build Order Reference Pattern'),
'description': _('Required pattern for generating Build Order reference field'),
'default': 'BO-{ref:04d}',
'validator': build.validators.validate_build_order_reference_pattern,
},
'BUILDORDER_REFERENCE_REGEX': {
'name': _('Build Order Reference Regex'),
'description': _('Regular expression pattern for matching build order reference')
},
'SALESORDER_REFERENCE_PREFIX': {
'name': _('Sales Order Reference Prefix'),
'description': _('Prefix value for sales order reference'),
'default': 'SO',
'SALESORDER_REFERENCE_PATTERN': {
'name': _('Sales Order Reference Pattern'),
'description': _('Required pattern for generating Sales Order reference field'),
'default': 'SO-{ref:04d}',
'validator': order.validators.validate_sales_order_reference_pattern,
},
'SALESORDER_DEFAULT_SHIPMENT': {
@ -1156,10 +1166,11 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
},
'PURCHASEORDER_REFERENCE_PREFIX': {
'name': _('Purchase Order Reference Prefix'),
'description': _('Prefix value for purchase order reference'),
'default': 'PO',
'PURCHASEORDER_REFERENCE_PATTERN': {
'name': _('Purchase Order Reference Pattern'),
'description': _('Required pattern for generating Purchase Order reference field'),
'default': 'PO-{ref:04d}',
'validator': order.validators.validate_purchase_order_reference_pattern,
},
# login / SSO

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-28 12:13+0000\n"
"POT-Creation-Date: 2022-07-04 02:07+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"
@ -141,7 +141,7 @@ msgstr ""
#: InvenTree/models.py:191 stock/models.py:2096
#: templates/js/translated/attachment.js:103
#: templates/js/translated/attachment.js:239
#: templates/js/translated/attachment.js:240
msgid "Attachment"
msgstr ""
@ -165,7 +165,7 @@ msgid "Link to external URL"
msgstr ""
#: InvenTree/models.py:202 templates/js/translated/attachment.js:104
#: templates/js/translated/attachment.js:283
#: templates/js/translated/attachment.js:284
msgid "Comment"
msgstr ""
@ -243,7 +243,7 @@ msgstr ""
#: stock/templates/stock/location.html:103
#: templates/InvenTree/settings/plugin_settings.html:33
#: templates/js/translated/bom.js:553 templates/js/translated/bom.js:845
#: templates/js/translated/build.js:2475 templates/js/translated/company.js:397
#: templates/js/translated/build.js:2476 templates/js/translated/company.js:397
#: templates/js/translated/company.js:653
#: templates/js/translated/company.js:944 templates/js/translated/order.js:1656
#: templates/js/translated/order.js:1888 templates/js/translated/order.js:2476
@ -656,7 +656,7 @@ msgstr ""
#: report/templates/report/inventree_build_order_base.html:106
#: templates/email/build_order_completed.html:16
#: templates/email/overdue_build_order.html:15
#: templates/js/translated/build.js:727
#: templates/js/translated/build.js:728
msgid "Build Order"
msgstr ""
@ -680,7 +680,7 @@ msgstr ""
#: report/templates/report/inventree_po_report.html:91
#: report/templates/report/inventree_so_report.html:92
#: templates/js/translated/bom.js:690 templates/js/translated/bom.js:852
#: templates/js/translated/build.js:1776 templates/js/translated/order.js:1919
#: templates/js/translated/build.js:1777 templates/js/translated/order.js:1919
#: templates/js/translated/order.js:2120 templates/js/translated/order.js:3468
#: templates/js/translated/order.js:3976
msgid "Reference"
@ -719,8 +719,8 @@ msgstr ""
#: templates/email/overdue_build_order.html:16
#: templates/js/translated/barcode.js:435 templates/js/translated/bom.js:552
#: templates/js/translated/bom.js:689 templates/js/translated/bom.js:799
#: templates/js/translated/build.js:1153 templates/js/translated/build.js:1646
#: templates/js/translated/build.js:2082 templates/js/translated/build.js:2480
#: templates/js/translated/build.js:1154 templates/js/translated/build.js:1647
#: templates/js/translated/build.js:2083 templates/js/translated/build.js:2481
#: templates/js/translated/company.js:255
#: templates/js/translated/company.js:484
#: templates/js/translated/company.js:594
@ -748,8 +748,8 @@ msgstr ""
msgid "SalesOrder to which this build is allocated"
msgstr ""
#: build/models.py:236 build/serializers.py:758
#: templates/js/translated/build.js:2070 templates/js/translated/order.js:2818
#: build/models.py:236 build/serializers.py:774
#: templates/js/translated/build.js:2071 templates/js/translated/order.js:2818
msgid "Source Location"
msgstr ""
@ -812,7 +812,7 @@ msgid "Target date for build completion. Build will be overdue after this date."
msgstr ""
#: build/models.py:289 order/models.py:323
#: templates/js/translated/build.js:2557
#: templates/js/translated/build.js:2558
msgid "Completion Date"
msgstr ""
@ -820,7 +820,7 @@ msgstr ""
msgid "completed by"
msgstr ""
#: build/models.py:303 templates/js/translated/build.js:2525
#: build/models.py:303 templates/js/translated/build.js:2526
msgid "Issued by"
msgstr ""
@ -833,7 +833,7 @@ msgstr ""
#: order/templates/order/order_base.html:176
#: order/templates/order/sales_order_base.html:183 part/models.py:935
#: report/templates/report/inventree_build_order_base.html:159
#: templates/js/translated/build.js:2537 templates/js/translated/order.js:1690
#: templates/js/translated/build.js:2538 templates/js/translated/order.js:1690
msgid "Responsible"
msgstr ""
@ -874,48 +874,48 @@ msgstr ""
msgid "Build output does not match Build Order"
msgstr ""
#: build/models.py:1148
#: build/models.py:1162
msgid "Build item must specify a build output, as master part is marked as trackable"
msgstr ""
#: build/models.py:1157
#: build/models.py:1171
#, python-brace-format
msgid "Allocated quantity ({q}) must not exceed available stock quantity ({a})"
msgstr ""
#: build/models.py:1167
#: build/models.py:1181
msgid "Stock item is over-allocated"
msgstr ""
#: build/models.py:1173 order/models.py:1404
#: build/models.py:1187 order/models.py:1404
msgid "Allocation quantity must be greater than zero"
msgstr ""
#: build/models.py:1179
#: build/models.py:1193
msgid "Quantity must be 1 for serialized stock"
msgstr ""
#: build/models.py:1236
#: build/models.py:1250
msgid "Selected stock item not found in BOM"
msgstr ""
#: build/models.py:1305 stock/templates/stock/item_base.html:177
#: templates/InvenTree/search.html:137 templates/js/translated/build.js:2453
#: build/models.py:1319 stock/templates/stock/item_base.html:177
#: templates/InvenTree/search.html:137 templates/js/translated/build.js:2454
#: templates/navbar.html:38
msgid "Build"
msgstr ""
#: build/models.py:1306
#: build/models.py:1320
msgid "Build to allocate parts"
msgstr ""
#: build/models.py:1322 build/serializers.py:603 order/serializers.py:996
#: build/models.py:1336 build/serializers.py:619 order/serializers.py:996
#: order/serializers.py:1017 stock/serializers.py:394 stock/serializers.py:741
#: stock/serializers.py:867 stock/templates/stock/item_base.html:10
#: stock/templates/stock/item_base.html:23
#: stock/templates/stock/item_base.html:199
#: templates/js/translated/build.js:738 templates/js/translated/build.js:743
#: templates/js/translated/build.js:2084 templates/js/translated/build.js:2642
#: templates/js/translated/build.js:739 templates/js/translated/build.js:744
#: templates/js/translated/build.js:2085 templates/js/translated/build.js:2643
#: templates/js/translated/order.js:101 templates/js/translated/order.js:2831
#: templates/js/translated/order.js:3135 templates/js/translated/order.js:3140
#: templates/js/translated/order.js:3235 templates/js/translated/order.js:3325
@ -924,11 +924,11 @@ msgstr ""
msgid "Stock Item"
msgstr ""
#: build/models.py:1323
#: build/models.py:1337
msgid "Source stock item"
msgstr ""
#: build/models.py:1335 build/serializers.py:183
#: build/models.py:1349 build/serializers.py:183
#: build/templates/build/build_base.html:82
#: build/templates/build/detail.html:34 common/models.py:1644
#: company/forms.py:36 company/templates/company/supplier_part.html:279
@ -947,10 +947,10 @@ msgstr ""
#: stock/templates/stock/item_base.html:300
#: templates/email/build_order_completed.html:18
#: templates/js/translated/barcode.js:437 templates/js/translated/bom.js:691
#: templates/js/translated/bom.js:860 templates/js/translated/build.js:422
#: templates/js/translated/build.js:574 templates/js/translated/build.js:765
#: templates/js/translated/build.js:1175 templates/js/translated/build.js:1672
#: templates/js/translated/build.js:2085
#: templates/js/translated/bom.js:860 templates/js/translated/build.js:423
#: templates/js/translated/build.js:575 templates/js/translated/build.js:766
#: templates/js/translated/build.js:1176 templates/js/translated/build.js:1673
#: templates/js/translated/build.js:2086
#: templates/js/translated/model_renderers.js:120
#: templates/js/translated/order.js:117 templates/js/translated/order.js:886
#: templates/js/translated/order.js:1925 templates/js/translated/order.js:2126
@ -965,20 +965,20 @@ msgstr ""
msgid "Quantity"
msgstr ""
#: build/models.py:1336
#: build/models.py:1350
msgid "Stock quantity to allocate to build"
msgstr ""
#: build/models.py:1344
#: build/models.py:1358
msgid "Install into"
msgstr ""
#: build/models.py:1345
#: build/models.py:1359
msgid "Destination stock item"
msgstr ""
#: build/serializers.py:128 build/serializers.py:632
#: templates/js/translated/build.js:1163
#: build/serializers.py:128 build/serializers.py:648
#: templates/js/translated/build.js:1164
msgid "Build Output"
msgstr ""
@ -1002,7 +1002,7 @@ msgstr ""
msgid "Enter quantity for build output"
msgstr ""
#: build/serializers.py:198 build/serializers.py:623 order/models.py:355
#: build/serializers.py:198 build/serializers.py:639 order/models.py:355
#: order/serializers.py:280 order/serializers.py:435 part/serializers.py:503
#: part/serializers.py:926 stock/models.py:478 stock/models.py:1244
#: stock/serializers.py:300
@ -1048,8 +1048,8 @@ msgstr ""
#: stock/serializers.py:902 stock/serializers.py:1135
#: stock/templates/stock/item_base.html:390
#: templates/js/translated/barcode.js:436
#: templates/js/translated/barcode.js:618 templates/js/translated/build.js:750
#: templates/js/translated/build.js:1684 templates/js/translated/order.js:1213
#: templates/js/translated/barcode.js:618 templates/js/translated/build.js:751
#: templates/js/translated/build.js:1685 templates/js/translated/order.js:1213
#: templates/js/translated/order.js:3147 templates/js/translated/order.js:3250
#: templates/js/translated/order.js:3258 templates/js/translated/order.js:3339
#: templates/js/translated/part.js:181 templates/js/translated/stock.js:584
@ -1065,7 +1065,7 @@ msgstr ""
#: build/serializers.py:363 build/templates/build/build_base.html:142
#: build/templates/build/detail.html:62 order/models.py:665
#: order/serializers.py:458 stock/templates/stock/item_base.html:423
#: templates/js/translated/barcode.js:182 templates/js/translated/build.js:2509
#: templates/js/translated/barcode.js:182 templates/js/translated/build.js:2510
#: templates/js/translated/order.js:1320 templates/js/translated/order.js:1660
#: templates/js/translated/order.js:2481 templates/js/translated/stock.js:1825
#: templates/js/translated/stock.js:2530 templates/js/translated/stock.js:2662
@ -1097,104 +1097,116 @@ msgid "Delete any build outputs which have not been completed"
msgstr ""
#: build/serializers.py:470
msgid "Accept Unallocated"
msgid "Accept Overallocated"
msgstr ""
#: build/serializers.py:471
msgid "Accept that stock items have not been fully allocated to this build order"
msgid "Accept stock items which have been overallocated to this build order"
msgstr ""
#: build/serializers.py:481 templates/js/translated/build.js:195
msgid "Required stock has not been fully allocated"
#: build/serializers.py:481
msgid "Some stock items have been overallocated"
msgstr ""
#: build/serializers.py:486
msgid "Accept Incomplete"
msgid "Accept Unallocated"
msgstr ""
#: build/serializers.py:487
msgid "Accept that stock items have not been fully allocated to this build order"
msgstr ""
#: build/serializers.py:497 templates/js/translated/build.js:196
msgid "Required stock has not been fully allocated"
msgstr ""
#: build/serializers.py:502
msgid "Accept Incomplete"
msgstr ""
#: build/serializers.py:503
msgid "Accept that the required number of build outputs have not been completed"
msgstr ""
#: build/serializers.py:497 templates/js/translated/build.js:199
#: build/serializers.py:513 templates/js/translated/build.js:200
msgid "Required build quantity has not been completed"
msgstr ""
#: build/serializers.py:506
#: build/serializers.py:522
msgid "Build order has incomplete outputs"
msgstr ""
#: build/serializers.py:509 build/templates/build/build_base.html:95
#: build/serializers.py:525 build/templates/build/build_base.html:95
msgid "No build outputs have been created for this build order"
msgstr ""
#: build/serializers.py:535 build/serializers.py:580 part/models.py:2719
#: build/serializers.py:551 build/serializers.py:596 part/models.py:2719
#: part/models.py:2853
msgid "BOM Item"
msgstr ""
#: build/serializers.py:545
#: build/serializers.py:561
msgid "Build output"
msgstr ""
#: build/serializers.py:553
#: build/serializers.py:569
msgid "Build output must point to the same build"
msgstr ""
#: build/serializers.py:594
#: build/serializers.py:610
msgid "bom_item.part must point to the same part as the build order"
msgstr ""
#: build/serializers.py:609 stock/serializers.py:754
#: build/serializers.py:625 stock/serializers.py:754
msgid "Item must be in stock"
msgstr ""
#: build/serializers.py:667 order/serializers.py:1054
#: build/serializers.py:683 order/serializers.py:1054
#, python-brace-format
msgid "Available quantity ({q}) exceeded"
msgstr ""
#: build/serializers.py:673
#: build/serializers.py:689
msgid "Build output must be specified for allocation of tracked parts"
msgstr ""
#: build/serializers.py:680
#: build/serializers.py:696
msgid "Build output cannot be specified for allocation of untracked parts"
msgstr ""
#: build/serializers.py:685
#: build/serializers.py:701
msgid "This stock item has already been allocated to this build output"
msgstr ""
#: build/serializers.py:708 order/serializers.py:1300
#: build/serializers.py:724 order/serializers.py:1300
msgid "Allocation items must be provided"
msgstr ""
#: build/serializers.py:759
#: build/serializers.py:775
msgid "Stock location where parts are to be sourced (leave blank to take from any location)"
msgstr ""
#: build/serializers.py:767
#: build/serializers.py:783
msgid "Exclude Location"
msgstr ""
#: build/serializers.py:768
#: build/serializers.py:784
msgid "Exclude stock items from this selected location"
msgstr ""
#: build/serializers.py:773
#: build/serializers.py:789
msgid "Interchangeable Stock"
msgstr ""
#: build/serializers.py:774
#: build/serializers.py:790
msgid "Stock items in multiple locations can be used interchangeably"
msgstr ""
#: build/serializers.py:779
#: build/serializers.py:795
msgid "Substitute Stock"
msgstr ""
#: build/serializers.py:780
#: build/serializers.py:796
msgid "Allow allocation of substitute parts"
msgstr ""
@ -1277,7 +1289,7 @@ msgstr ""
#: order/templates/order/order_base.html:162
#: order/templates/order/sales_order_base.html:164
#: report/templates/report/inventree_build_order_base.html:126
#: templates/js/translated/build.js:2549 templates/js/translated/order.js:1677
#: templates/js/translated/build.js:2550 templates/js/translated/order.js:1677
#: templates/js/translated/order.js:1987 templates/js/translated/order.js:2497
#: templates/js/translated/order.js:3537 templates/js/translated/part.js:1040
msgid "Target Date"
@ -1364,7 +1376,7 @@ msgstr ""
#: build/templates/build/detail.html:80
#: stock/templates/stock/item_base.html:170
#: templates/js/translated/build.js:1179
#: templates/js/translated/build.js:1180
#: templates/js/translated/model_renderers.js:124
#: templates/js/translated/stock.js:1022 templates/js/translated/stock.js:1839
#: templates/js/translated/stock.js:2669
@ -1376,7 +1388,7 @@ msgstr ""
#: build/templates/build/detail.html:126
#: order/templates/order/order_base.html:149
#: order/templates/order/sales_order_base.html:158
#: templates/js/translated/build.js:2517
#: templates/js/translated/build.js:2518
msgid "Created"
msgstr ""
@ -1396,7 +1408,7 @@ msgstr ""
msgid "Allocate Stock to Build"
msgstr ""
#: build/templates/build/detail.html:176 templates/js/translated/build.js:1898
#: build/templates/build/detail.html:176 templates/js/translated/build.js:1899
msgid "Unallocate stock"
msgstr ""
@ -2868,8 +2880,8 @@ msgstr ""
#: company/models.py:538 company/templates/company/supplier_part.html:94
#: templates/email/build_order_required_stock.html:19
#: templates/email/low_stock_notification.html:18
#: templates/js/translated/bom.js:884 templates/js/translated/build.js:1786
#: templates/js/translated/build.js:2649 templates/js/translated/company.js:959
#: templates/js/translated/bom.js:884 templates/js/translated/build.js:1787
#: templates/js/translated/build.js:2650 templates/js/translated/company.js:959
#: templates/js/translated/part.js:596 templates/js/translated/part.js:599
#: templates/js/translated/table_filters.js:178
msgid "Available"
@ -3063,7 +3075,7 @@ msgid "New Sales Order"
msgstr ""
#: company/templates/company/detail.html:168
#: templates/js/translated/build.js:1657
#: templates/js/translated/build.js:1658
msgid "Assigned Stock"
msgstr ""
@ -3985,8 +3997,8 @@ msgstr ""
#: part/templates/part/import_wizard/ajax_match_fields.html:64
#: part/templates/part/import_wizard/ajax_match_references.html:42
#: part/templates/part/import_wizard/match_references.html:49
#: templates/js/translated/bom.js:77 templates/js/translated/build.js:427
#: templates/js/translated/build.js:579 templates/js/translated/build.js:1971
#: templates/js/translated/bom.js:77 templates/js/translated/build.js:428
#: templates/js/translated/build.js:580 templates/js/translated/build.js:1972
#: templates/js/translated/order.js:833 templates/js/translated/order.js:1265
#: templates/js/translated/order.js:2742 templates/js/translated/stock.js:621
#: templates/js/translated/stock.js:789
@ -4109,7 +4121,7 @@ msgstr ""
#: order/templates/order/sales_order_detail.html:72
#: templates/attachment_table.html:6 templates/js/translated/bom.js:1047
#: templates/js/translated/build.js:1879
#: templates/js/translated/build.js:1880
msgid "Actions"
msgstr ""
@ -4167,19 +4179,19 @@ msgstr ""
msgid "This option must be selected"
msgstr ""
#: part/api.py:1073
#: part/api.py:1074
msgid "Must be greater than zero"
msgstr ""
#: part/api.py:1077
#: part/api.py:1078
msgid "Must be a valid quantity"
msgstr ""
#: part/api.py:1092
#: part/api.py:1093
msgid "Specify location for initial part stock"
msgstr ""
#: part/api.py:1123 part/api.py:1127 part/api.py:1142 part/api.py:1146
#: part/api.py:1124 part/api.py:1128 part/api.py:1143 part/api.py:1147
msgid "This field is required"
msgstr ""
@ -5773,8 +5785,8 @@ msgstr ""
#: report/templates/report/inventree_test_report_base.html:79
#: stock/models.py:641 stock/templates/stock/item_base.html:322
#: templates/js/translated/build.js:420 templates/js/translated/build.js:572
#: templates/js/translated/build.js:1173 templates/js/translated/build.js:1670
#: templates/js/translated/build.js:421 templates/js/translated/build.js:573
#: templates/js/translated/build.js:1174 templates/js/translated/build.js:1671
#: templates/js/translated/model_renderers.js:118
#: templates/js/translated/order.js:115 templates/js/translated/order.js:3240
#: templates/js/translated/order.js:3329 templates/js/translated/stock.js:486
@ -6395,7 +6407,7 @@ msgid "Available Quantity"
msgstr ""
#: stock/templates/stock/item_base.html:394
#: templates/js/translated/build.js:1692
#: templates/js/translated/build.js:1693
msgid "No location set"
msgstr ""
@ -6808,7 +6820,7 @@ msgid "Plugin Settings"
msgstr ""
#: templates/InvenTree/settings/plugin.html:16
msgid "Changing the settings below require you to immediatly restart the server. Do not change this while under active usage."
msgid "Changing the settings below require you to immediately restart the server. Do not change this while under active usage."
msgstr ""
#: templates/InvenTree/settings/plugin.html:34
@ -7689,19 +7701,19 @@ msgstr ""
msgid "No attachments found"
msgstr ""
#: templates/js/translated/attachment.js:218
#: templates/js/translated/attachment.js:219
msgid "Edit Attachment"
msgstr ""
#: templates/js/translated/attachment.js:287
#: templates/js/translated/attachment.js:289
msgid "Upload Date"
msgstr ""
#: templates/js/translated/attachment.js:310
#: templates/js/translated/attachment.js:312
msgid "Edit attachment"
msgstr ""
#: templates/js/translated/attachment.js:319
#: templates/js/translated/attachment.js:321
msgid "Delete attachment"
msgstr ""
@ -7921,25 +7933,25 @@ msgstr ""
msgid "Substitutes Available"
msgstr ""
#: templates/js/translated/bom.js:832 templates/js/translated/build.js:1768
#: templates/js/translated/bom.js:832 templates/js/translated/build.js:1769
msgid "Variant stock allowed"
msgstr ""
#: templates/js/translated/bom.js:900 templates/js/translated/build.js:1813
#: templates/js/translated/bom.js:900 templates/js/translated/build.js:1814
#: templates/js/translated/order.js:3577
msgid "No Stock Available"
msgstr ""
#: templates/js/translated/bom.js:904 templates/js/translated/build.js:1817
#: templates/js/translated/bom.js:904 templates/js/translated/build.js:1818
msgid "Includes variant and substitute stock"
msgstr ""
#: templates/js/translated/bom.js:906 templates/js/translated/build.js:1819
#: templates/js/translated/bom.js:906 templates/js/translated/build.js:1820
#: templates/js/translated/part.js:759
msgid "Includes variant stock"
msgstr ""
#: templates/js/translated/bom.js:908 templates/js/translated/build.js:1821
#: templates/js/translated/bom.js:908 templates/js/translated/build.js:1822
msgid "Includes substitute stock"
msgstr ""
@ -7979,11 +7991,11 @@ msgstr ""
msgid "Delete BOM Item"
msgstr ""
#: templates/js/translated/bom.js:1158 templates/js/translated/build.js:1614
#: templates/js/translated/bom.js:1158 templates/js/translated/build.js:1615
msgid "No BOM items found"
msgstr ""
#: templates/js/translated/bom.js:1402 templates/js/translated/build.js:1752
#: templates/js/translated/bom.js:1402 templates/js/translated/build.js:1753
msgid "Required Part"
msgstr ""
@ -8015,256 +8027,256 @@ msgstr ""
msgid "There are incomplete outputs remaining for this build order"
msgstr ""
#: templates/js/translated/build.js:185
#: templates/js/translated/build.js:186
msgid "Build order is ready to be completed"
msgstr ""
#: templates/js/translated/build.js:190
#: templates/js/translated/build.js:191
msgid "Build Order is incomplete"
msgstr ""
#: templates/js/translated/build.js:218
#: templates/js/translated/build.js:219
msgid "Complete Build Order"
msgstr ""
#: templates/js/translated/build.js:259 templates/js/translated/stock.js:92
#: templates/js/translated/build.js:260 templates/js/translated/stock.js:92
#: templates/js/translated/stock.js:210
msgid "Next available serial number"
msgstr ""
#: templates/js/translated/build.js:261 templates/js/translated/stock.js:94
#: templates/js/translated/build.js:262 templates/js/translated/stock.js:94
#: templates/js/translated/stock.js:212
msgid "Latest serial number"
msgstr ""
#: templates/js/translated/build.js:270
#: templates/js/translated/build.js:271
msgid "The Bill of Materials contains trackable parts"
msgstr ""
#: templates/js/translated/build.js:271
#: templates/js/translated/build.js:272
msgid "Build outputs must be generated individually"
msgstr ""
#: templates/js/translated/build.js:279
#: templates/js/translated/build.js:280
msgid "Trackable parts can have serial numbers specified"
msgstr ""
#: templates/js/translated/build.js:280
#: templates/js/translated/build.js:281
msgid "Enter serial numbers to generate multiple single build outputs"
msgstr ""
#: templates/js/translated/build.js:287
#: templates/js/translated/build.js:288
msgid "Create Build Output"
msgstr ""
#: templates/js/translated/build.js:318
#: templates/js/translated/build.js:319
msgid "Allocate stock items to this build output"
msgstr ""
#: templates/js/translated/build.js:329
#: templates/js/translated/build.js:330
msgid "Unallocate stock from build output"
msgstr ""
#: templates/js/translated/build.js:338
#: templates/js/translated/build.js:339
msgid "Complete build output"
msgstr ""
#: templates/js/translated/build.js:346
#: templates/js/translated/build.js:347
msgid "Delete build output"
msgstr ""
#: templates/js/translated/build.js:369
#: templates/js/translated/build.js:370
msgid "Are you sure you wish to unallocate stock items from this build?"
msgstr ""
#: templates/js/translated/build.js:387
#: templates/js/translated/build.js:388
msgid "Unallocate Stock Items"
msgstr ""
#: templates/js/translated/build.js:407 templates/js/translated/build.js:559
#: templates/js/translated/build.js:408 templates/js/translated/build.js:560
msgid "Select Build Outputs"
msgstr ""
#: templates/js/translated/build.js:408 templates/js/translated/build.js:560
#: templates/js/translated/build.js:409 templates/js/translated/build.js:561
msgid "At least one build output must be selected"
msgstr ""
#: templates/js/translated/build.js:462 templates/js/translated/build.js:614
#: templates/js/translated/build.js:463 templates/js/translated/build.js:615
msgid "Output"
msgstr ""
#: templates/js/translated/build.js:480
#: templates/js/translated/build.js:481
msgid "Complete Build Outputs"
msgstr ""
#: templates/js/translated/build.js:627
#: templates/js/translated/build.js:628
msgid "Delete Build Outputs"
msgstr ""
#: templates/js/translated/build.js:716
#: templates/js/translated/build.js:717
msgid "No build order allocations found"
msgstr ""
#: templates/js/translated/build.js:754
#: templates/js/translated/build.js:755
msgid "Location not specified"
msgstr ""
#: templates/js/translated/build.js:1133
#: templates/js/translated/build.js:1134
msgid "No active build outputs found"
msgstr ""
#: templates/js/translated/build.js:1202
#: templates/js/translated/build.js:1203
msgid "Allocated Stock"
msgstr ""
#: templates/js/translated/build.js:1209
#: templates/js/translated/build.js:1210
msgid "No tracked BOM items for this build"
msgstr ""
#: templates/js/translated/build.js:1231
#: templates/js/translated/build.js:1232
msgid "Completed Tests"
msgstr ""
#: templates/js/translated/build.js:1236
#: templates/js/translated/build.js:1237
msgid "No required tests for this build"
msgstr ""
#: templates/js/translated/build.js:1709 templates/js/translated/build.js:2660
#: templates/js/translated/build.js:1710 templates/js/translated/build.js:2661
#: templates/js/translated/order.js:3277
msgid "Edit stock allocation"
msgstr ""
#: templates/js/translated/build.js:1711 templates/js/translated/build.js:2661
#: templates/js/translated/build.js:1712 templates/js/translated/build.js:2662
#: templates/js/translated/order.js:3278
msgid "Delete stock allocation"
msgstr ""
#: templates/js/translated/build.js:1729
#: templates/js/translated/build.js:1730
msgid "Edit Allocation"
msgstr ""
#: templates/js/translated/build.js:1739
#: templates/js/translated/build.js:1740
msgid "Remove Allocation"
msgstr ""
#: templates/js/translated/build.js:1764
#: templates/js/translated/build.js:1765
msgid "Substitute parts available"
msgstr ""
#: templates/js/translated/build.js:1781
#: templates/js/translated/build.js:1782
msgid "Quantity Per"
msgstr ""
#: templates/js/translated/build.js:1807 templates/js/translated/order.js:3584
#: templates/js/translated/build.js:1808 templates/js/translated/order.js:3584
msgid "Insufficient stock available"
msgstr ""
#: templates/js/translated/build.js:1809 templates/js/translated/order.js:3582
#: templates/js/translated/build.js:1810 templates/js/translated/order.js:3582
msgid "Sufficient stock available"
msgstr ""
#: templates/js/translated/build.js:1838 templates/js/translated/build.js:2083
#: templates/js/translated/build.js:2656 templates/js/translated/order.js:3596
#: templates/js/translated/build.js:1839 templates/js/translated/build.js:2084
#: templates/js/translated/build.js:2657 templates/js/translated/order.js:3596
msgid "Allocated"
msgstr ""
#: templates/js/translated/build.js:1886 templates/js/translated/order.js:3676
#: templates/js/translated/build.js:1887 templates/js/translated/order.js:3676
msgid "Build stock"
msgstr ""
#: templates/js/translated/build.js:1890 templates/stock_table.html:50
#: templates/js/translated/build.js:1891 templates/stock_table.html:50
msgid "Order stock"
msgstr ""
#: templates/js/translated/build.js:1893 templates/js/translated/order.js:3669
#: templates/js/translated/build.js:1894 templates/js/translated/order.js:3669
msgid "Allocate stock"
msgstr ""
#: templates/js/translated/build.js:1932 templates/js/translated/label.js:172
#: templates/js/translated/build.js:1933 templates/js/translated/label.js:172
#: templates/js/translated/order.js:756 templates/js/translated/order.js:2804
#: templates/js/translated/report.js:225
msgid "Select Parts"
msgstr ""
#: templates/js/translated/build.js:1933 templates/js/translated/order.js:2805
#: templates/js/translated/build.js:1934 templates/js/translated/order.js:2805
msgid "You must select at least one part to allocate"
msgstr ""
#: templates/js/translated/build.js:1982 templates/js/translated/order.js:2753
#: templates/js/translated/build.js:1983 templates/js/translated/order.js:2753
msgid "Specify stock allocation quantity"
msgstr ""
#: templates/js/translated/build.js:2056
#: templates/js/translated/build.js:2057
msgid "All Parts Allocated"
msgstr ""
#: templates/js/translated/build.js:2057
#: templates/js/translated/build.js:2058
msgid "All selected parts have been fully allocated"
msgstr ""
#: templates/js/translated/build.js:2071 templates/js/translated/order.js:2819
#: templates/js/translated/build.js:2072 templates/js/translated/order.js:2819
msgid "Select source location (leave blank to take from all locations)"
msgstr ""
#: templates/js/translated/build.js:2099
#: templates/js/translated/build.js:2100
msgid "Allocate Stock Items to Build Order"
msgstr ""
#: templates/js/translated/build.js:2110 templates/js/translated/order.js:2916
#: templates/js/translated/build.js:2111 templates/js/translated/order.js:2916
msgid "No matching stock locations"
msgstr ""
#: templates/js/translated/build.js:2182 templates/js/translated/order.js:2993
#: templates/js/translated/build.js:2183 templates/js/translated/order.js:2993
msgid "No matching stock items"
msgstr ""
#: templates/js/translated/build.js:2279
#: templates/js/translated/build.js:2280
msgid "Automatic Stock Allocation"
msgstr ""
#: templates/js/translated/build.js:2280
#: templates/js/translated/build.js:2281
msgid "Stock items will be automatically allocated to this build order, according to the provided guidelines"
msgstr ""
#: templates/js/translated/build.js:2282
#: templates/js/translated/build.js:2283
msgid "If a location is specifed, stock will only be allocated from that location"
msgstr ""
#: templates/js/translated/build.js:2283
#: templates/js/translated/build.js:2284
msgid "If stock is considered interchangeable, it will be allocated from the first location it is found"
msgstr ""
#: templates/js/translated/build.js:2284
#: templates/js/translated/build.js:2285
msgid "If substitute stock is allowed, it will be used where stock of the primary part cannot be found"
msgstr ""
#: templates/js/translated/build.js:2305
#: templates/js/translated/build.js:2306
msgid "Allocate Stock Items"
msgstr ""
#: templates/js/translated/build.js:2412
#: templates/js/translated/build.js:2413
msgid "No builds matching query"
msgstr ""
#: templates/js/translated/build.js:2447 templates/js/translated/part.js:1383
#: templates/js/translated/build.js:2448 templates/js/translated/part.js:1383
#: templates/js/translated/part.js:1850 templates/js/translated/stock.js:1682
#: templates/js/translated/stock.js:2340
msgid "Select"
msgstr ""
#: templates/js/translated/build.js:2467
#: templates/js/translated/build.js:2468
msgid "Build order is overdue"
msgstr ""
#: templates/js/translated/build.js:2495
#: templates/js/translated/build.js:2496
msgid "Progress"
msgstr ""
#: templates/js/translated/build.js:2531 templates/js/translated/stock.js:2582
#: templates/js/translated/build.js:2532 templates/js/translated/stock.js:2582
msgid "No user information"
msgstr ""
#: templates/js/translated/build.js:2637
#: templates/js/translated/build.js:2638
msgid "No parts allocated for"
msgstr ""
@ -10058,61 +10070,61 @@ msgstr ""
msgid "Select File Format"
msgstr ""
#: templates/js/translated/tables.js:534
#: templates/js/translated/tables.js:535
msgid "Loading data"
msgstr ""
#: templates/js/translated/tables.js:537
#: templates/js/translated/tables.js:538
msgid "rows per page"
msgstr ""
#: templates/js/translated/tables.js:542
#: templates/js/translated/tables.js:543
msgid "Showing all rows"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "Showing"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "to"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "of"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "rows"
msgstr ""
#: templates/js/translated/tables.js:548 templates/navbar.html:102
#: templates/js/translated/tables.js:549 templates/navbar.html:102
#: templates/search.html:8 templates/search_form.html:6
#: templates/search_form.html:7
msgid "Search"
msgstr ""
#: templates/js/translated/tables.js:551
#: templates/js/translated/tables.js:552
msgid "No matching results"
msgstr ""
#: templates/js/translated/tables.js:554
#: templates/js/translated/tables.js:555
msgid "Hide/Show pagination"
msgstr ""
#: templates/js/translated/tables.js:557
#: templates/js/translated/tables.js:558
msgid "Refresh"
msgstr ""
#: templates/js/translated/tables.js:560
#: templates/js/translated/tables.js:561
msgid "Toggle"
msgstr ""
#: templates/js/translated/tables.js:563
#: templates/js/translated/tables.js:564
msgid "Columns"
msgstr ""
#: templates/js/translated/tables.js:566
#: templates/js/translated/tables.js:567
msgid "All"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-28 12:13+0000\n"
"POT-Creation-Date: 2022-07-04 02:07+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"
@ -141,7 +141,7 @@ msgstr ""
#: InvenTree/models.py:191 stock/models.py:2096
#: templates/js/translated/attachment.js:103
#: templates/js/translated/attachment.js:239
#: templates/js/translated/attachment.js:240
msgid "Attachment"
msgstr ""
@ -165,7 +165,7 @@ msgid "Link to external URL"
msgstr ""
#: InvenTree/models.py:202 templates/js/translated/attachment.js:104
#: templates/js/translated/attachment.js:283
#: templates/js/translated/attachment.js:284
msgid "Comment"
msgstr ""
@ -243,7 +243,7 @@ msgstr ""
#: stock/templates/stock/location.html:103
#: templates/InvenTree/settings/plugin_settings.html:33
#: templates/js/translated/bom.js:553 templates/js/translated/bom.js:845
#: templates/js/translated/build.js:2475 templates/js/translated/company.js:397
#: templates/js/translated/build.js:2476 templates/js/translated/company.js:397
#: templates/js/translated/company.js:653
#: templates/js/translated/company.js:944 templates/js/translated/order.js:1656
#: templates/js/translated/order.js:1888 templates/js/translated/order.js:2476
@ -656,7 +656,7 @@ msgstr ""
#: report/templates/report/inventree_build_order_base.html:106
#: templates/email/build_order_completed.html:16
#: templates/email/overdue_build_order.html:15
#: templates/js/translated/build.js:727
#: templates/js/translated/build.js:728
msgid "Build Order"
msgstr ""
@ -680,7 +680,7 @@ msgstr ""
#: report/templates/report/inventree_po_report.html:91
#: report/templates/report/inventree_so_report.html:92
#: templates/js/translated/bom.js:690 templates/js/translated/bom.js:852
#: templates/js/translated/build.js:1776 templates/js/translated/order.js:1919
#: templates/js/translated/build.js:1777 templates/js/translated/order.js:1919
#: templates/js/translated/order.js:2120 templates/js/translated/order.js:3468
#: templates/js/translated/order.js:3976
msgid "Reference"
@ -719,8 +719,8 @@ msgstr ""
#: templates/email/overdue_build_order.html:16
#: templates/js/translated/barcode.js:435 templates/js/translated/bom.js:552
#: templates/js/translated/bom.js:689 templates/js/translated/bom.js:799
#: templates/js/translated/build.js:1153 templates/js/translated/build.js:1646
#: templates/js/translated/build.js:2082 templates/js/translated/build.js:2480
#: templates/js/translated/build.js:1154 templates/js/translated/build.js:1647
#: templates/js/translated/build.js:2083 templates/js/translated/build.js:2481
#: templates/js/translated/company.js:255
#: templates/js/translated/company.js:484
#: templates/js/translated/company.js:594
@ -748,8 +748,8 @@ msgstr ""
msgid "SalesOrder to which this build is allocated"
msgstr ""
#: build/models.py:236 build/serializers.py:758
#: templates/js/translated/build.js:2070 templates/js/translated/order.js:2818
#: build/models.py:236 build/serializers.py:774
#: templates/js/translated/build.js:2071 templates/js/translated/order.js:2818
msgid "Source Location"
msgstr ""
@ -812,7 +812,7 @@ msgid "Target date for build completion. Build will be overdue after this date."
msgstr ""
#: build/models.py:289 order/models.py:323
#: templates/js/translated/build.js:2557
#: templates/js/translated/build.js:2558
msgid "Completion Date"
msgstr ""
@ -820,7 +820,7 @@ msgstr ""
msgid "completed by"
msgstr ""
#: build/models.py:303 templates/js/translated/build.js:2525
#: build/models.py:303 templates/js/translated/build.js:2526
msgid "Issued by"
msgstr ""
@ -833,7 +833,7 @@ msgstr ""
#: order/templates/order/order_base.html:176
#: order/templates/order/sales_order_base.html:183 part/models.py:935
#: report/templates/report/inventree_build_order_base.html:159
#: templates/js/translated/build.js:2537 templates/js/translated/order.js:1690
#: templates/js/translated/build.js:2538 templates/js/translated/order.js:1690
msgid "Responsible"
msgstr ""
@ -874,48 +874,48 @@ msgstr ""
msgid "Build output does not match Build Order"
msgstr ""
#: build/models.py:1148
#: build/models.py:1162
msgid "Build item must specify a build output, as master part is marked as trackable"
msgstr ""
#: build/models.py:1157
#: build/models.py:1171
#, python-brace-format
msgid "Allocated quantity ({q}) must not exceed available stock quantity ({a})"
msgstr ""
#: build/models.py:1167
#: build/models.py:1181
msgid "Stock item is over-allocated"
msgstr ""
#: build/models.py:1173 order/models.py:1404
#: build/models.py:1187 order/models.py:1404
msgid "Allocation quantity must be greater than zero"
msgstr ""
#: build/models.py:1179
#: build/models.py:1193
msgid "Quantity must be 1 for serialized stock"
msgstr ""
#: build/models.py:1236
#: build/models.py:1250
msgid "Selected stock item not found in BOM"
msgstr ""
#: build/models.py:1305 stock/templates/stock/item_base.html:177
#: templates/InvenTree/search.html:137 templates/js/translated/build.js:2453
#: build/models.py:1319 stock/templates/stock/item_base.html:177
#: templates/InvenTree/search.html:137 templates/js/translated/build.js:2454
#: templates/navbar.html:38
msgid "Build"
msgstr ""
#: build/models.py:1306
#: build/models.py:1320
msgid "Build to allocate parts"
msgstr ""
#: build/models.py:1322 build/serializers.py:603 order/serializers.py:996
#: build/models.py:1336 build/serializers.py:619 order/serializers.py:996
#: order/serializers.py:1017 stock/serializers.py:394 stock/serializers.py:741
#: stock/serializers.py:867 stock/templates/stock/item_base.html:10
#: stock/templates/stock/item_base.html:23
#: stock/templates/stock/item_base.html:199
#: templates/js/translated/build.js:738 templates/js/translated/build.js:743
#: templates/js/translated/build.js:2084 templates/js/translated/build.js:2642
#: templates/js/translated/build.js:739 templates/js/translated/build.js:744
#: templates/js/translated/build.js:2085 templates/js/translated/build.js:2643
#: templates/js/translated/order.js:101 templates/js/translated/order.js:2831
#: templates/js/translated/order.js:3135 templates/js/translated/order.js:3140
#: templates/js/translated/order.js:3235 templates/js/translated/order.js:3325
@ -924,11 +924,11 @@ msgstr ""
msgid "Stock Item"
msgstr ""
#: build/models.py:1323
#: build/models.py:1337
msgid "Source stock item"
msgstr ""
#: build/models.py:1335 build/serializers.py:183
#: build/models.py:1349 build/serializers.py:183
#: build/templates/build/build_base.html:82
#: build/templates/build/detail.html:34 common/models.py:1644
#: company/forms.py:36 company/templates/company/supplier_part.html:279
@ -947,10 +947,10 @@ msgstr ""
#: stock/templates/stock/item_base.html:300
#: templates/email/build_order_completed.html:18
#: templates/js/translated/barcode.js:437 templates/js/translated/bom.js:691
#: templates/js/translated/bom.js:860 templates/js/translated/build.js:422
#: templates/js/translated/build.js:574 templates/js/translated/build.js:765
#: templates/js/translated/build.js:1175 templates/js/translated/build.js:1672
#: templates/js/translated/build.js:2085
#: templates/js/translated/bom.js:860 templates/js/translated/build.js:423
#: templates/js/translated/build.js:575 templates/js/translated/build.js:766
#: templates/js/translated/build.js:1176 templates/js/translated/build.js:1673
#: templates/js/translated/build.js:2086
#: templates/js/translated/model_renderers.js:120
#: templates/js/translated/order.js:117 templates/js/translated/order.js:886
#: templates/js/translated/order.js:1925 templates/js/translated/order.js:2126
@ -965,20 +965,20 @@ msgstr ""
msgid "Quantity"
msgstr ""
#: build/models.py:1336
#: build/models.py:1350
msgid "Stock quantity to allocate to build"
msgstr ""
#: build/models.py:1344
#: build/models.py:1358
msgid "Install into"
msgstr ""
#: build/models.py:1345
#: build/models.py:1359
msgid "Destination stock item"
msgstr ""
#: build/serializers.py:128 build/serializers.py:632
#: templates/js/translated/build.js:1163
#: build/serializers.py:128 build/serializers.py:648
#: templates/js/translated/build.js:1164
msgid "Build Output"
msgstr ""
@ -1002,7 +1002,7 @@ msgstr ""
msgid "Enter quantity for build output"
msgstr ""
#: build/serializers.py:198 build/serializers.py:623 order/models.py:355
#: build/serializers.py:198 build/serializers.py:639 order/models.py:355
#: order/serializers.py:280 order/serializers.py:435 part/serializers.py:503
#: part/serializers.py:926 stock/models.py:478 stock/models.py:1244
#: stock/serializers.py:300
@ -1048,8 +1048,8 @@ msgstr ""
#: stock/serializers.py:902 stock/serializers.py:1135
#: stock/templates/stock/item_base.html:390
#: templates/js/translated/barcode.js:436
#: templates/js/translated/barcode.js:618 templates/js/translated/build.js:750
#: templates/js/translated/build.js:1684 templates/js/translated/order.js:1213
#: templates/js/translated/barcode.js:618 templates/js/translated/build.js:751
#: templates/js/translated/build.js:1685 templates/js/translated/order.js:1213
#: templates/js/translated/order.js:3147 templates/js/translated/order.js:3250
#: templates/js/translated/order.js:3258 templates/js/translated/order.js:3339
#: templates/js/translated/part.js:181 templates/js/translated/stock.js:584
@ -1065,7 +1065,7 @@ msgstr ""
#: build/serializers.py:363 build/templates/build/build_base.html:142
#: build/templates/build/detail.html:62 order/models.py:665
#: order/serializers.py:458 stock/templates/stock/item_base.html:423
#: templates/js/translated/barcode.js:182 templates/js/translated/build.js:2509
#: templates/js/translated/barcode.js:182 templates/js/translated/build.js:2510
#: templates/js/translated/order.js:1320 templates/js/translated/order.js:1660
#: templates/js/translated/order.js:2481 templates/js/translated/stock.js:1825
#: templates/js/translated/stock.js:2530 templates/js/translated/stock.js:2662
@ -1097,104 +1097,116 @@ msgid "Delete any build outputs which have not been completed"
msgstr ""
#: build/serializers.py:470
msgid "Accept Unallocated"
msgid "Accept Overallocated"
msgstr ""
#: build/serializers.py:471
msgid "Accept that stock items have not been fully allocated to this build order"
msgid "Accept stock items which have been overallocated to this build order"
msgstr ""
#: build/serializers.py:481 templates/js/translated/build.js:195
msgid "Required stock has not been fully allocated"
#: build/serializers.py:481
msgid "Some stock items have been overallocated"
msgstr ""
#: build/serializers.py:486
msgid "Accept Incomplete"
msgid "Accept Unallocated"
msgstr ""
#: build/serializers.py:487
msgid "Accept that stock items have not been fully allocated to this build order"
msgstr ""
#: build/serializers.py:497 templates/js/translated/build.js:196
msgid "Required stock has not been fully allocated"
msgstr ""
#: build/serializers.py:502
msgid "Accept Incomplete"
msgstr ""
#: build/serializers.py:503
msgid "Accept that the required number of build outputs have not been completed"
msgstr ""
#: build/serializers.py:497 templates/js/translated/build.js:199
#: build/serializers.py:513 templates/js/translated/build.js:200
msgid "Required build quantity has not been completed"
msgstr ""
#: build/serializers.py:506
#: build/serializers.py:522
msgid "Build order has incomplete outputs"
msgstr ""
#: build/serializers.py:509 build/templates/build/build_base.html:95
#: build/serializers.py:525 build/templates/build/build_base.html:95
msgid "No build outputs have been created for this build order"
msgstr ""
#: build/serializers.py:535 build/serializers.py:580 part/models.py:2719
#: build/serializers.py:551 build/serializers.py:596 part/models.py:2719
#: part/models.py:2853
msgid "BOM Item"
msgstr ""
#: build/serializers.py:545
#: build/serializers.py:561
msgid "Build output"
msgstr ""
#: build/serializers.py:553
#: build/serializers.py:569
msgid "Build output must point to the same build"
msgstr ""
#: build/serializers.py:594
#: build/serializers.py:610
msgid "bom_item.part must point to the same part as the build order"
msgstr ""
#: build/serializers.py:609 stock/serializers.py:754
#: build/serializers.py:625 stock/serializers.py:754
msgid "Item must be in stock"
msgstr ""
#: build/serializers.py:667 order/serializers.py:1054
#: build/serializers.py:683 order/serializers.py:1054
#, python-brace-format
msgid "Available quantity ({q}) exceeded"
msgstr ""
#: build/serializers.py:673
#: build/serializers.py:689
msgid "Build output must be specified for allocation of tracked parts"
msgstr ""
#: build/serializers.py:680
#: build/serializers.py:696
msgid "Build output cannot be specified for allocation of untracked parts"
msgstr ""
#: build/serializers.py:685
#: build/serializers.py:701
msgid "This stock item has already been allocated to this build output"
msgstr ""
#: build/serializers.py:708 order/serializers.py:1300
#: build/serializers.py:724 order/serializers.py:1300
msgid "Allocation items must be provided"
msgstr ""
#: build/serializers.py:759
#: build/serializers.py:775
msgid "Stock location where parts are to be sourced (leave blank to take from any location)"
msgstr ""
#: build/serializers.py:767
#: build/serializers.py:783
msgid "Exclude Location"
msgstr ""
#: build/serializers.py:768
#: build/serializers.py:784
msgid "Exclude stock items from this selected location"
msgstr ""
#: build/serializers.py:773
#: build/serializers.py:789
msgid "Interchangeable Stock"
msgstr ""
#: build/serializers.py:774
#: build/serializers.py:790
msgid "Stock items in multiple locations can be used interchangeably"
msgstr ""
#: build/serializers.py:779
#: build/serializers.py:795
msgid "Substitute Stock"
msgstr ""
#: build/serializers.py:780
#: build/serializers.py:796
msgid "Allow allocation of substitute parts"
msgstr ""
@ -1277,7 +1289,7 @@ msgstr ""
#: order/templates/order/order_base.html:162
#: order/templates/order/sales_order_base.html:164
#: report/templates/report/inventree_build_order_base.html:126
#: templates/js/translated/build.js:2549 templates/js/translated/order.js:1677
#: templates/js/translated/build.js:2550 templates/js/translated/order.js:1677
#: templates/js/translated/order.js:1987 templates/js/translated/order.js:2497
#: templates/js/translated/order.js:3537 templates/js/translated/part.js:1040
msgid "Target Date"
@ -1364,7 +1376,7 @@ msgstr ""
#: build/templates/build/detail.html:80
#: stock/templates/stock/item_base.html:170
#: templates/js/translated/build.js:1179
#: templates/js/translated/build.js:1180
#: templates/js/translated/model_renderers.js:124
#: templates/js/translated/stock.js:1022 templates/js/translated/stock.js:1839
#: templates/js/translated/stock.js:2669
@ -1376,7 +1388,7 @@ msgstr ""
#: build/templates/build/detail.html:126
#: order/templates/order/order_base.html:149
#: order/templates/order/sales_order_base.html:158
#: templates/js/translated/build.js:2517
#: templates/js/translated/build.js:2518
msgid "Created"
msgstr ""
@ -1396,7 +1408,7 @@ msgstr ""
msgid "Allocate Stock to Build"
msgstr ""
#: build/templates/build/detail.html:176 templates/js/translated/build.js:1898
#: build/templates/build/detail.html:176 templates/js/translated/build.js:1899
msgid "Unallocate stock"
msgstr ""
@ -2868,8 +2880,8 @@ msgstr ""
#: company/models.py:538 company/templates/company/supplier_part.html:94
#: templates/email/build_order_required_stock.html:19
#: templates/email/low_stock_notification.html:18
#: templates/js/translated/bom.js:884 templates/js/translated/build.js:1786
#: templates/js/translated/build.js:2649 templates/js/translated/company.js:959
#: templates/js/translated/bom.js:884 templates/js/translated/build.js:1787
#: templates/js/translated/build.js:2650 templates/js/translated/company.js:959
#: templates/js/translated/part.js:596 templates/js/translated/part.js:599
#: templates/js/translated/table_filters.js:178
msgid "Available"
@ -3063,7 +3075,7 @@ msgid "New Sales Order"
msgstr ""
#: company/templates/company/detail.html:168
#: templates/js/translated/build.js:1657
#: templates/js/translated/build.js:1658
msgid "Assigned Stock"
msgstr ""
@ -3985,8 +3997,8 @@ msgstr ""
#: part/templates/part/import_wizard/ajax_match_fields.html:64
#: part/templates/part/import_wizard/ajax_match_references.html:42
#: part/templates/part/import_wizard/match_references.html:49
#: templates/js/translated/bom.js:77 templates/js/translated/build.js:427
#: templates/js/translated/build.js:579 templates/js/translated/build.js:1971
#: templates/js/translated/bom.js:77 templates/js/translated/build.js:428
#: templates/js/translated/build.js:580 templates/js/translated/build.js:1972
#: templates/js/translated/order.js:833 templates/js/translated/order.js:1265
#: templates/js/translated/order.js:2742 templates/js/translated/stock.js:621
#: templates/js/translated/stock.js:789
@ -4109,7 +4121,7 @@ msgstr ""
#: order/templates/order/sales_order_detail.html:72
#: templates/attachment_table.html:6 templates/js/translated/bom.js:1047
#: templates/js/translated/build.js:1879
#: templates/js/translated/build.js:1880
msgid "Actions"
msgstr ""
@ -4167,19 +4179,19 @@ msgstr ""
msgid "This option must be selected"
msgstr ""
#: part/api.py:1073
#: part/api.py:1074
msgid "Must be greater than zero"
msgstr ""
#: part/api.py:1077
#: part/api.py:1078
msgid "Must be a valid quantity"
msgstr ""
#: part/api.py:1092
#: part/api.py:1093
msgid "Specify location for initial part stock"
msgstr ""
#: part/api.py:1123 part/api.py:1127 part/api.py:1142 part/api.py:1146
#: part/api.py:1124 part/api.py:1128 part/api.py:1143 part/api.py:1147
msgid "This field is required"
msgstr ""
@ -5773,8 +5785,8 @@ msgstr ""
#: report/templates/report/inventree_test_report_base.html:79
#: stock/models.py:641 stock/templates/stock/item_base.html:322
#: templates/js/translated/build.js:420 templates/js/translated/build.js:572
#: templates/js/translated/build.js:1173 templates/js/translated/build.js:1670
#: templates/js/translated/build.js:421 templates/js/translated/build.js:573
#: templates/js/translated/build.js:1174 templates/js/translated/build.js:1671
#: templates/js/translated/model_renderers.js:118
#: templates/js/translated/order.js:115 templates/js/translated/order.js:3240
#: templates/js/translated/order.js:3329 templates/js/translated/stock.js:486
@ -6395,7 +6407,7 @@ msgid "Available Quantity"
msgstr ""
#: stock/templates/stock/item_base.html:394
#: templates/js/translated/build.js:1692
#: templates/js/translated/build.js:1693
msgid "No location set"
msgstr ""
@ -6808,7 +6820,7 @@ msgid "Plugin Settings"
msgstr ""
#: templates/InvenTree/settings/plugin.html:16
msgid "Changing the settings below require you to immediatly restart the server. Do not change this while under active usage."
msgid "Changing the settings below require you to immediately restart the server. Do not change this while under active usage."
msgstr ""
#: templates/InvenTree/settings/plugin.html:34
@ -7689,19 +7701,19 @@ msgstr ""
msgid "No attachments found"
msgstr ""
#: templates/js/translated/attachment.js:218
#: templates/js/translated/attachment.js:219
msgid "Edit Attachment"
msgstr ""
#: templates/js/translated/attachment.js:287
#: templates/js/translated/attachment.js:289
msgid "Upload Date"
msgstr ""
#: templates/js/translated/attachment.js:310
#: templates/js/translated/attachment.js:312
msgid "Edit attachment"
msgstr ""
#: templates/js/translated/attachment.js:319
#: templates/js/translated/attachment.js:321
msgid "Delete attachment"
msgstr ""
@ -7921,25 +7933,25 @@ msgstr ""
msgid "Substitutes Available"
msgstr ""
#: templates/js/translated/bom.js:832 templates/js/translated/build.js:1768
#: templates/js/translated/bom.js:832 templates/js/translated/build.js:1769
msgid "Variant stock allowed"
msgstr ""
#: templates/js/translated/bom.js:900 templates/js/translated/build.js:1813
#: templates/js/translated/bom.js:900 templates/js/translated/build.js:1814
#: templates/js/translated/order.js:3577
msgid "No Stock Available"
msgstr ""
#: templates/js/translated/bom.js:904 templates/js/translated/build.js:1817
#: templates/js/translated/bom.js:904 templates/js/translated/build.js:1818
msgid "Includes variant and substitute stock"
msgstr ""
#: templates/js/translated/bom.js:906 templates/js/translated/build.js:1819
#: templates/js/translated/bom.js:906 templates/js/translated/build.js:1820
#: templates/js/translated/part.js:759
msgid "Includes variant stock"
msgstr ""
#: templates/js/translated/bom.js:908 templates/js/translated/build.js:1821
#: templates/js/translated/bom.js:908 templates/js/translated/build.js:1822
msgid "Includes substitute stock"
msgstr ""
@ -7979,11 +7991,11 @@ msgstr ""
msgid "Delete BOM Item"
msgstr ""
#: templates/js/translated/bom.js:1158 templates/js/translated/build.js:1614
#: templates/js/translated/bom.js:1158 templates/js/translated/build.js:1615
msgid "No BOM items found"
msgstr ""
#: templates/js/translated/bom.js:1402 templates/js/translated/build.js:1752
#: templates/js/translated/bom.js:1402 templates/js/translated/build.js:1753
msgid "Required Part"
msgstr ""
@ -8015,256 +8027,256 @@ msgstr ""
msgid "There are incomplete outputs remaining for this build order"
msgstr ""
#: templates/js/translated/build.js:185
#: templates/js/translated/build.js:186
msgid "Build order is ready to be completed"
msgstr ""
#: templates/js/translated/build.js:190
#: templates/js/translated/build.js:191
msgid "Build Order is incomplete"
msgstr ""
#: templates/js/translated/build.js:218
#: templates/js/translated/build.js:219
msgid "Complete Build Order"
msgstr ""
#: templates/js/translated/build.js:259 templates/js/translated/stock.js:92
#: templates/js/translated/build.js:260 templates/js/translated/stock.js:92
#: templates/js/translated/stock.js:210
msgid "Next available serial number"
msgstr ""
#: templates/js/translated/build.js:261 templates/js/translated/stock.js:94
#: templates/js/translated/build.js:262 templates/js/translated/stock.js:94
#: templates/js/translated/stock.js:212
msgid "Latest serial number"
msgstr ""
#: templates/js/translated/build.js:270
#: templates/js/translated/build.js:271
msgid "The Bill of Materials contains trackable parts"
msgstr ""
#: templates/js/translated/build.js:271
#: templates/js/translated/build.js:272
msgid "Build outputs must be generated individually"
msgstr ""
#: templates/js/translated/build.js:279
#: templates/js/translated/build.js:280
msgid "Trackable parts can have serial numbers specified"
msgstr ""
#: templates/js/translated/build.js:280
#: templates/js/translated/build.js:281
msgid "Enter serial numbers to generate multiple single build outputs"
msgstr ""
#: templates/js/translated/build.js:287
#: templates/js/translated/build.js:288
msgid "Create Build Output"
msgstr ""
#: templates/js/translated/build.js:318
#: templates/js/translated/build.js:319
msgid "Allocate stock items to this build output"
msgstr ""
#: templates/js/translated/build.js:329
#: templates/js/translated/build.js:330
msgid "Unallocate stock from build output"
msgstr ""
#: templates/js/translated/build.js:338
#: templates/js/translated/build.js:339
msgid "Complete build output"
msgstr ""
#: templates/js/translated/build.js:346
#: templates/js/translated/build.js:347
msgid "Delete build output"
msgstr ""
#: templates/js/translated/build.js:369
#: templates/js/translated/build.js:370
msgid "Are you sure you wish to unallocate stock items from this build?"
msgstr ""
#: templates/js/translated/build.js:387
#: templates/js/translated/build.js:388
msgid "Unallocate Stock Items"
msgstr ""
#: templates/js/translated/build.js:407 templates/js/translated/build.js:559
#: templates/js/translated/build.js:408 templates/js/translated/build.js:560
msgid "Select Build Outputs"
msgstr ""
#: templates/js/translated/build.js:408 templates/js/translated/build.js:560
#: templates/js/translated/build.js:409 templates/js/translated/build.js:561
msgid "At least one build output must be selected"
msgstr ""
#: templates/js/translated/build.js:462 templates/js/translated/build.js:614
#: templates/js/translated/build.js:463 templates/js/translated/build.js:615
msgid "Output"
msgstr ""
#: templates/js/translated/build.js:480
#: templates/js/translated/build.js:481
msgid "Complete Build Outputs"
msgstr ""
#: templates/js/translated/build.js:627
#: templates/js/translated/build.js:628
msgid "Delete Build Outputs"
msgstr ""
#: templates/js/translated/build.js:716
#: templates/js/translated/build.js:717
msgid "No build order allocations found"
msgstr ""
#: templates/js/translated/build.js:754
#: templates/js/translated/build.js:755
msgid "Location not specified"
msgstr ""
#: templates/js/translated/build.js:1133
#: templates/js/translated/build.js:1134
msgid "No active build outputs found"
msgstr ""
#: templates/js/translated/build.js:1202
#: templates/js/translated/build.js:1203
msgid "Allocated Stock"
msgstr ""
#: templates/js/translated/build.js:1209
#: templates/js/translated/build.js:1210
msgid "No tracked BOM items for this build"
msgstr ""
#: templates/js/translated/build.js:1231
#: templates/js/translated/build.js:1232
msgid "Completed Tests"
msgstr ""
#: templates/js/translated/build.js:1236
#: templates/js/translated/build.js:1237
msgid "No required tests for this build"
msgstr ""
#: templates/js/translated/build.js:1709 templates/js/translated/build.js:2660
#: templates/js/translated/build.js:1710 templates/js/translated/build.js:2661
#: templates/js/translated/order.js:3277
msgid "Edit stock allocation"
msgstr ""
#: templates/js/translated/build.js:1711 templates/js/translated/build.js:2661
#: templates/js/translated/build.js:1712 templates/js/translated/build.js:2662
#: templates/js/translated/order.js:3278
msgid "Delete stock allocation"
msgstr ""
#: templates/js/translated/build.js:1729
#: templates/js/translated/build.js:1730
msgid "Edit Allocation"
msgstr ""
#: templates/js/translated/build.js:1739
#: templates/js/translated/build.js:1740
msgid "Remove Allocation"
msgstr ""
#: templates/js/translated/build.js:1764
#: templates/js/translated/build.js:1765
msgid "Substitute parts available"
msgstr ""
#: templates/js/translated/build.js:1781
#: templates/js/translated/build.js:1782
msgid "Quantity Per"
msgstr ""
#: templates/js/translated/build.js:1807 templates/js/translated/order.js:3584
#: templates/js/translated/build.js:1808 templates/js/translated/order.js:3584
msgid "Insufficient stock available"
msgstr ""
#: templates/js/translated/build.js:1809 templates/js/translated/order.js:3582
#: templates/js/translated/build.js:1810 templates/js/translated/order.js:3582
msgid "Sufficient stock available"
msgstr ""
#: templates/js/translated/build.js:1838 templates/js/translated/build.js:2083
#: templates/js/translated/build.js:2656 templates/js/translated/order.js:3596
#: templates/js/translated/build.js:1839 templates/js/translated/build.js:2084
#: templates/js/translated/build.js:2657 templates/js/translated/order.js:3596
msgid "Allocated"
msgstr ""
#: templates/js/translated/build.js:1886 templates/js/translated/order.js:3676
#: templates/js/translated/build.js:1887 templates/js/translated/order.js:3676
msgid "Build stock"
msgstr ""
#: templates/js/translated/build.js:1890 templates/stock_table.html:50
#: templates/js/translated/build.js:1891 templates/stock_table.html:50
msgid "Order stock"
msgstr ""
#: templates/js/translated/build.js:1893 templates/js/translated/order.js:3669
#: templates/js/translated/build.js:1894 templates/js/translated/order.js:3669
msgid "Allocate stock"
msgstr ""
#: templates/js/translated/build.js:1932 templates/js/translated/label.js:172
#: templates/js/translated/build.js:1933 templates/js/translated/label.js:172
#: templates/js/translated/order.js:756 templates/js/translated/order.js:2804
#: templates/js/translated/report.js:225
msgid "Select Parts"
msgstr ""
#: templates/js/translated/build.js:1933 templates/js/translated/order.js:2805
#: templates/js/translated/build.js:1934 templates/js/translated/order.js:2805
msgid "You must select at least one part to allocate"
msgstr ""
#: templates/js/translated/build.js:1982 templates/js/translated/order.js:2753
#: templates/js/translated/build.js:1983 templates/js/translated/order.js:2753
msgid "Specify stock allocation quantity"
msgstr ""
#: templates/js/translated/build.js:2056
#: templates/js/translated/build.js:2057
msgid "All Parts Allocated"
msgstr ""
#: templates/js/translated/build.js:2057
#: templates/js/translated/build.js:2058
msgid "All selected parts have been fully allocated"
msgstr ""
#: templates/js/translated/build.js:2071 templates/js/translated/order.js:2819
#: templates/js/translated/build.js:2072 templates/js/translated/order.js:2819
msgid "Select source location (leave blank to take from all locations)"
msgstr ""
#: templates/js/translated/build.js:2099
#: templates/js/translated/build.js:2100
msgid "Allocate Stock Items to Build Order"
msgstr ""
#: templates/js/translated/build.js:2110 templates/js/translated/order.js:2916
#: templates/js/translated/build.js:2111 templates/js/translated/order.js:2916
msgid "No matching stock locations"
msgstr ""
#: templates/js/translated/build.js:2182 templates/js/translated/order.js:2993
#: templates/js/translated/build.js:2183 templates/js/translated/order.js:2993
msgid "No matching stock items"
msgstr ""
#: templates/js/translated/build.js:2279
#: templates/js/translated/build.js:2280
msgid "Automatic Stock Allocation"
msgstr ""
#: templates/js/translated/build.js:2280
#: templates/js/translated/build.js:2281
msgid "Stock items will be automatically allocated to this build order, according to the provided guidelines"
msgstr ""
#: templates/js/translated/build.js:2282
#: templates/js/translated/build.js:2283
msgid "If a location is specifed, stock will only be allocated from that location"
msgstr ""
#: templates/js/translated/build.js:2283
#: templates/js/translated/build.js:2284
msgid "If stock is considered interchangeable, it will be allocated from the first location it is found"
msgstr ""
#: templates/js/translated/build.js:2284
#: templates/js/translated/build.js:2285
msgid "If substitute stock is allowed, it will be used where stock of the primary part cannot be found"
msgstr ""
#: templates/js/translated/build.js:2305
#: templates/js/translated/build.js:2306
msgid "Allocate Stock Items"
msgstr ""
#: templates/js/translated/build.js:2412
#: templates/js/translated/build.js:2413
msgid "No builds matching query"
msgstr ""
#: templates/js/translated/build.js:2447 templates/js/translated/part.js:1383
#: templates/js/translated/build.js:2448 templates/js/translated/part.js:1383
#: templates/js/translated/part.js:1850 templates/js/translated/stock.js:1682
#: templates/js/translated/stock.js:2340
msgid "Select"
msgstr ""
#: templates/js/translated/build.js:2467
#: templates/js/translated/build.js:2468
msgid "Build order is overdue"
msgstr ""
#: templates/js/translated/build.js:2495
#: templates/js/translated/build.js:2496
msgid "Progress"
msgstr ""
#: templates/js/translated/build.js:2531 templates/js/translated/stock.js:2582
#: templates/js/translated/build.js:2532 templates/js/translated/stock.js:2582
msgid "No user information"
msgstr ""
#: templates/js/translated/build.js:2637
#: templates/js/translated/build.js:2638
msgid "No parts allocated for"
msgstr ""
@ -10058,61 +10070,61 @@ msgstr ""
msgid "Select File Format"
msgstr ""
#: templates/js/translated/tables.js:534
#: templates/js/translated/tables.js:535
msgid "Loading data"
msgstr ""
#: templates/js/translated/tables.js:537
#: templates/js/translated/tables.js:538
msgid "rows per page"
msgstr ""
#: templates/js/translated/tables.js:542
#: templates/js/translated/tables.js:543
msgid "Showing all rows"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "Showing"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "to"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "of"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "rows"
msgstr ""
#: templates/js/translated/tables.js:548 templates/navbar.html:102
#: templates/js/translated/tables.js:549 templates/navbar.html:102
#: templates/search.html:8 templates/search_form.html:6
#: templates/search_form.html:7
msgid "Search"
msgstr ""
#: templates/js/translated/tables.js:551
#: templates/js/translated/tables.js:552
msgid "No matching results"
msgstr ""
#: templates/js/translated/tables.js:554
#: templates/js/translated/tables.js:555
msgid "Hide/Show pagination"
msgstr ""
#: templates/js/translated/tables.js:557
#: templates/js/translated/tables.js:558
msgid "Refresh"
msgstr ""
#: templates/js/translated/tables.js:560
#: templates/js/translated/tables.js:561
msgid "Toggle"
msgstr ""
#: templates/js/translated/tables.js:563
#: templates/js/translated/tables.js:564
msgid "Columns"
msgstr ""
#: templates/js/translated/tables.js:566
#: templates/js/translated/tables.js:567
msgid "All"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-28 12:13+0000\n"
"POT-Creation-Date: 2022-07-04 02:07+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"
@ -141,7 +141,7 @@ msgstr ""
#: InvenTree/models.py:191 stock/models.py:2096
#: templates/js/translated/attachment.js:103
#: templates/js/translated/attachment.js:239
#: templates/js/translated/attachment.js:240
msgid "Attachment"
msgstr ""
@ -165,7 +165,7 @@ msgid "Link to external URL"
msgstr ""
#: InvenTree/models.py:202 templates/js/translated/attachment.js:104
#: templates/js/translated/attachment.js:283
#: templates/js/translated/attachment.js:284
msgid "Comment"
msgstr ""
@ -243,7 +243,7 @@ msgstr ""
#: stock/templates/stock/location.html:103
#: templates/InvenTree/settings/plugin_settings.html:33
#: templates/js/translated/bom.js:553 templates/js/translated/bom.js:845
#: templates/js/translated/build.js:2475 templates/js/translated/company.js:397
#: templates/js/translated/build.js:2476 templates/js/translated/company.js:397
#: templates/js/translated/company.js:653
#: templates/js/translated/company.js:944 templates/js/translated/order.js:1656
#: templates/js/translated/order.js:1888 templates/js/translated/order.js:2476
@ -656,7 +656,7 @@ msgstr ""
#: report/templates/report/inventree_build_order_base.html:106
#: templates/email/build_order_completed.html:16
#: templates/email/overdue_build_order.html:15
#: templates/js/translated/build.js:727
#: templates/js/translated/build.js:728
msgid "Build Order"
msgstr ""
@ -680,7 +680,7 @@ msgstr ""
#: report/templates/report/inventree_po_report.html:91
#: report/templates/report/inventree_so_report.html:92
#: templates/js/translated/bom.js:690 templates/js/translated/bom.js:852
#: templates/js/translated/build.js:1776 templates/js/translated/order.js:1919
#: templates/js/translated/build.js:1777 templates/js/translated/order.js:1919
#: templates/js/translated/order.js:2120 templates/js/translated/order.js:3468
#: templates/js/translated/order.js:3976
msgid "Reference"
@ -719,8 +719,8 @@ msgstr ""
#: templates/email/overdue_build_order.html:16
#: templates/js/translated/barcode.js:435 templates/js/translated/bom.js:552
#: templates/js/translated/bom.js:689 templates/js/translated/bom.js:799
#: templates/js/translated/build.js:1153 templates/js/translated/build.js:1646
#: templates/js/translated/build.js:2082 templates/js/translated/build.js:2480
#: templates/js/translated/build.js:1154 templates/js/translated/build.js:1647
#: templates/js/translated/build.js:2083 templates/js/translated/build.js:2481
#: templates/js/translated/company.js:255
#: templates/js/translated/company.js:484
#: templates/js/translated/company.js:594
@ -748,8 +748,8 @@ msgstr ""
msgid "SalesOrder to which this build is allocated"
msgstr ""
#: build/models.py:236 build/serializers.py:758
#: templates/js/translated/build.js:2070 templates/js/translated/order.js:2818
#: build/models.py:236 build/serializers.py:774
#: templates/js/translated/build.js:2071 templates/js/translated/order.js:2818
msgid "Source Location"
msgstr ""
@ -812,7 +812,7 @@ msgid "Target date for build completion. Build will be overdue after this date."
msgstr ""
#: build/models.py:289 order/models.py:323
#: templates/js/translated/build.js:2557
#: templates/js/translated/build.js:2558
msgid "Completion Date"
msgstr ""
@ -820,7 +820,7 @@ msgstr ""
msgid "completed by"
msgstr ""
#: build/models.py:303 templates/js/translated/build.js:2525
#: build/models.py:303 templates/js/translated/build.js:2526
msgid "Issued by"
msgstr ""
@ -833,7 +833,7 @@ msgstr ""
#: order/templates/order/order_base.html:176
#: order/templates/order/sales_order_base.html:183 part/models.py:935
#: report/templates/report/inventree_build_order_base.html:159
#: templates/js/translated/build.js:2537 templates/js/translated/order.js:1690
#: templates/js/translated/build.js:2538 templates/js/translated/order.js:1690
msgid "Responsible"
msgstr ""
@ -874,48 +874,48 @@ msgstr ""
msgid "Build output does not match Build Order"
msgstr ""
#: build/models.py:1148
#: build/models.py:1162
msgid "Build item must specify a build output, as master part is marked as trackable"
msgstr ""
#: build/models.py:1157
#: build/models.py:1171
#, python-brace-format
msgid "Allocated quantity ({q}) must not exceed available stock quantity ({a})"
msgstr ""
#: build/models.py:1167
#: build/models.py:1181
msgid "Stock item is over-allocated"
msgstr ""
#: build/models.py:1173 order/models.py:1404
#: build/models.py:1187 order/models.py:1404
msgid "Allocation quantity must be greater than zero"
msgstr ""
#: build/models.py:1179
#: build/models.py:1193
msgid "Quantity must be 1 for serialized stock"
msgstr ""
#: build/models.py:1236
#: build/models.py:1250
msgid "Selected stock item not found in BOM"
msgstr ""
#: build/models.py:1305 stock/templates/stock/item_base.html:177
#: templates/InvenTree/search.html:137 templates/js/translated/build.js:2453
#: build/models.py:1319 stock/templates/stock/item_base.html:177
#: templates/InvenTree/search.html:137 templates/js/translated/build.js:2454
#: templates/navbar.html:38
msgid "Build"
msgstr ""
#: build/models.py:1306
#: build/models.py:1320
msgid "Build to allocate parts"
msgstr ""
#: build/models.py:1322 build/serializers.py:603 order/serializers.py:996
#: build/models.py:1336 build/serializers.py:619 order/serializers.py:996
#: order/serializers.py:1017 stock/serializers.py:394 stock/serializers.py:741
#: stock/serializers.py:867 stock/templates/stock/item_base.html:10
#: stock/templates/stock/item_base.html:23
#: stock/templates/stock/item_base.html:199
#: templates/js/translated/build.js:738 templates/js/translated/build.js:743
#: templates/js/translated/build.js:2084 templates/js/translated/build.js:2642
#: templates/js/translated/build.js:739 templates/js/translated/build.js:744
#: templates/js/translated/build.js:2085 templates/js/translated/build.js:2643
#: templates/js/translated/order.js:101 templates/js/translated/order.js:2831
#: templates/js/translated/order.js:3135 templates/js/translated/order.js:3140
#: templates/js/translated/order.js:3235 templates/js/translated/order.js:3325
@ -924,11 +924,11 @@ msgstr ""
msgid "Stock Item"
msgstr ""
#: build/models.py:1323
#: build/models.py:1337
msgid "Source stock item"
msgstr ""
#: build/models.py:1335 build/serializers.py:183
#: build/models.py:1349 build/serializers.py:183
#: build/templates/build/build_base.html:82
#: build/templates/build/detail.html:34 common/models.py:1644
#: company/forms.py:36 company/templates/company/supplier_part.html:279
@ -947,10 +947,10 @@ msgstr ""
#: stock/templates/stock/item_base.html:300
#: templates/email/build_order_completed.html:18
#: templates/js/translated/barcode.js:437 templates/js/translated/bom.js:691
#: templates/js/translated/bom.js:860 templates/js/translated/build.js:422
#: templates/js/translated/build.js:574 templates/js/translated/build.js:765
#: templates/js/translated/build.js:1175 templates/js/translated/build.js:1672
#: templates/js/translated/build.js:2085
#: templates/js/translated/bom.js:860 templates/js/translated/build.js:423
#: templates/js/translated/build.js:575 templates/js/translated/build.js:766
#: templates/js/translated/build.js:1176 templates/js/translated/build.js:1673
#: templates/js/translated/build.js:2086
#: templates/js/translated/model_renderers.js:120
#: templates/js/translated/order.js:117 templates/js/translated/order.js:886
#: templates/js/translated/order.js:1925 templates/js/translated/order.js:2126
@ -965,20 +965,20 @@ msgstr ""
msgid "Quantity"
msgstr ""
#: build/models.py:1336
#: build/models.py:1350
msgid "Stock quantity to allocate to build"
msgstr ""
#: build/models.py:1344
#: build/models.py:1358
msgid "Install into"
msgstr ""
#: build/models.py:1345
#: build/models.py:1359
msgid "Destination stock item"
msgstr ""
#: build/serializers.py:128 build/serializers.py:632
#: templates/js/translated/build.js:1163
#: build/serializers.py:128 build/serializers.py:648
#: templates/js/translated/build.js:1164
msgid "Build Output"
msgstr ""
@ -1002,7 +1002,7 @@ msgstr ""
msgid "Enter quantity for build output"
msgstr ""
#: build/serializers.py:198 build/serializers.py:623 order/models.py:355
#: build/serializers.py:198 build/serializers.py:639 order/models.py:355
#: order/serializers.py:280 order/serializers.py:435 part/serializers.py:503
#: part/serializers.py:926 stock/models.py:478 stock/models.py:1244
#: stock/serializers.py:300
@ -1048,8 +1048,8 @@ msgstr ""
#: stock/serializers.py:902 stock/serializers.py:1135
#: stock/templates/stock/item_base.html:390
#: templates/js/translated/barcode.js:436
#: templates/js/translated/barcode.js:618 templates/js/translated/build.js:750
#: templates/js/translated/build.js:1684 templates/js/translated/order.js:1213
#: templates/js/translated/barcode.js:618 templates/js/translated/build.js:751
#: templates/js/translated/build.js:1685 templates/js/translated/order.js:1213
#: templates/js/translated/order.js:3147 templates/js/translated/order.js:3250
#: templates/js/translated/order.js:3258 templates/js/translated/order.js:3339
#: templates/js/translated/part.js:181 templates/js/translated/stock.js:584
@ -1065,7 +1065,7 @@ msgstr ""
#: build/serializers.py:363 build/templates/build/build_base.html:142
#: build/templates/build/detail.html:62 order/models.py:665
#: order/serializers.py:458 stock/templates/stock/item_base.html:423
#: templates/js/translated/barcode.js:182 templates/js/translated/build.js:2509
#: templates/js/translated/barcode.js:182 templates/js/translated/build.js:2510
#: templates/js/translated/order.js:1320 templates/js/translated/order.js:1660
#: templates/js/translated/order.js:2481 templates/js/translated/stock.js:1825
#: templates/js/translated/stock.js:2530 templates/js/translated/stock.js:2662
@ -1097,104 +1097,116 @@ msgid "Delete any build outputs which have not been completed"
msgstr ""
#: build/serializers.py:470
msgid "Accept Unallocated"
msgid "Accept Overallocated"
msgstr ""
#: build/serializers.py:471
msgid "Accept that stock items have not been fully allocated to this build order"
msgid "Accept stock items which have been overallocated to this build order"
msgstr ""
#: build/serializers.py:481 templates/js/translated/build.js:195
msgid "Required stock has not been fully allocated"
#: build/serializers.py:481
msgid "Some stock items have been overallocated"
msgstr ""
#: build/serializers.py:486
msgid "Accept Incomplete"
msgid "Accept Unallocated"
msgstr ""
#: build/serializers.py:487
msgid "Accept that stock items have not been fully allocated to this build order"
msgstr ""
#: build/serializers.py:497 templates/js/translated/build.js:196
msgid "Required stock has not been fully allocated"
msgstr ""
#: build/serializers.py:502
msgid "Accept Incomplete"
msgstr ""
#: build/serializers.py:503
msgid "Accept that the required number of build outputs have not been completed"
msgstr ""
#: build/serializers.py:497 templates/js/translated/build.js:199
#: build/serializers.py:513 templates/js/translated/build.js:200
msgid "Required build quantity has not been completed"
msgstr ""
#: build/serializers.py:506
#: build/serializers.py:522
msgid "Build order has incomplete outputs"
msgstr ""
#: build/serializers.py:509 build/templates/build/build_base.html:95
#: build/serializers.py:525 build/templates/build/build_base.html:95
msgid "No build outputs have been created for this build order"
msgstr ""
#: build/serializers.py:535 build/serializers.py:580 part/models.py:2719
#: build/serializers.py:551 build/serializers.py:596 part/models.py:2719
#: part/models.py:2853
msgid "BOM Item"
msgstr ""
#: build/serializers.py:545
#: build/serializers.py:561
msgid "Build output"
msgstr ""
#: build/serializers.py:553
#: build/serializers.py:569
msgid "Build output must point to the same build"
msgstr ""
#: build/serializers.py:594
#: build/serializers.py:610
msgid "bom_item.part must point to the same part as the build order"
msgstr ""
#: build/serializers.py:609 stock/serializers.py:754
#: build/serializers.py:625 stock/serializers.py:754
msgid "Item must be in stock"
msgstr ""
#: build/serializers.py:667 order/serializers.py:1054
#: build/serializers.py:683 order/serializers.py:1054
#, python-brace-format
msgid "Available quantity ({q}) exceeded"
msgstr ""
#: build/serializers.py:673
#: build/serializers.py:689
msgid "Build output must be specified for allocation of tracked parts"
msgstr ""
#: build/serializers.py:680
#: build/serializers.py:696
msgid "Build output cannot be specified for allocation of untracked parts"
msgstr ""
#: build/serializers.py:685
#: build/serializers.py:701
msgid "This stock item has already been allocated to this build output"
msgstr ""
#: build/serializers.py:708 order/serializers.py:1300
#: build/serializers.py:724 order/serializers.py:1300
msgid "Allocation items must be provided"
msgstr ""
#: build/serializers.py:759
#: build/serializers.py:775
msgid "Stock location where parts are to be sourced (leave blank to take from any location)"
msgstr ""
#: build/serializers.py:767
#: build/serializers.py:783
msgid "Exclude Location"
msgstr ""
#: build/serializers.py:768
#: build/serializers.py:784
msgid "Exclude stock items from this selected location"
msgstr ""
#: build/serializers.py:773
#: build/serializers.py:789
msgid "Interchangeable Stock"
msgstr ""
#: build/serializers.py:774
#: build/serializers.py:790
msgid "Stock items in multiple locations can be used interchangeably"
msgstr ""
#: build/serializers.py:779
#: build/serializers.py:795
msgid "Substitute Stock"
msgstr ""
#: build/serializers.py:780
#: build/serializers.py:796
msgid "Allow allocation of substitute parts"
msgstr ""
@ -1277,7 +1289,7 @@ msgstr ""
#: order/templates/order/order_base.html:162
#: order/templates/order/sales_order_base.html:164
#: report/templates/report/inventree_build_order_base.html:126
#: templates/js/translated/build.js:2549 templates/js/translated/order.js:1677
#: templates/js/translated/build.js:2550 templates/js/translated/order.js:1677
#: templates/js/translated/order.js:1987 templates/js/translated/order.js:2497
#: templates/js/translated/order.js:3537 templates/js/translated/part.js:1040
msgid "Target Date"
@ -1364,7 +1376,7 @@ msgstr ""
#: build/templates/build/detail.html:80
#: stock/templates/stock/item_base.html:170
#: templates/js/translated/build.js:1179
#: templates/js/translated/build.js:1180
#: templates/js/translated/model_renderers.js:124
#: templates/js/translated/stock.js:1022 templates/js/translated/stock.js:1839
#: templates/js/translated/stock.js:2669
@ -1376,7 +1388,7 @@ msgstr ""
#: build/templates/build/detail.html:126
#: order/templates/order/order_base.html:149
#: order/templates/order/sales_order_base.html:158
#: templates/js/translated/build.js:2517
#: templates/js/translated/build.js:2518
msgid "Created"
msgstr ""
@ -1396,7 +1408,7 @@ msgstr ""
msgid "Allocate Stock to Build"
msgstr ""
#: build/templates/build/detail.html:176 templates/js/translated/build.js:1898
#: build/templates/build/detail.html:176 templates/js/translated/build.js:1899
msgid "Unallocate stock"
msgstr ""
@ -2868,8 +2880,8 @@ msgstr ""
#: company/models.py:538 company/templates/company/supplier_part.html:94
#: templates/email/build_order_required_stock.html:19
#: templates/email/low_stock_notification.html:18
#: templates/js/translated/bom.js:884 templates/js/translated/build.js:1786
#: templates/js/translated/build.js:2649 templates/js/translated/company.js:959
#: templates/js/translated/bom.js:884 templates/js/translated/build.js:1787
#: templates/js/translated/build.js:2650 templates/js/translated/company.js:959
#: templates/js/translated/part.js:596 templates/js/translated/part.js:599
#: templates/js/translated/table_filters.js:178
msgid "Available"
@ -3063,7 +3075,7 @@ msgid "New Sales Order"
msgstr ""
#: company/templates/company/detail.html:168
#: templates/js/translated/build.js:1657
#: templates/js/translated/build.js:1658
msgid "Assigned Stock"
msgstr ""
@ -3985,8 +3997,8 @@ msgstr ""
#: part/templates/part/import_wizard/ajax_match_fields.html:64
#: part/templates/part/import_wizard/ajax_match_references.html:42
#: part/templates/part/import_wizard/match_references.html:49
#: templates/js/translated/bom.js:77 templates/js/translated/build.js:427
#: templates/js/translated/build.js:579 templates/js/translated/build.js:1971
#: templates/js/translated/bom.js:77 templates/js/translated/build.js:428
#: templates/js/translated/build.js:580 templates/js/translated/build.js:1972
#: templates/js/translated/order.js:833 templates/js/translated/order.js:1265
#: templates/js/translated/order.js:2742 templates/js/translated/stock.js:621
#: templates/js/translated/stock.js:789
@ -4109,7 +4121,7 @@ msgstr ""
#: order/templates/order/sales_order_detail.html:72
#: templates/attachment_table.html:6 templates/js/translated/bom.js:1047
#: templates/js/translated/build.js:1879
#: templates/js/translated/build.js:1880
msgid "Actions"
msgstr ""
@ -4167,19 +4179,19 @@ msgstr ""
msgid "This option must be selected"
msgstr ""
#: part/api.py:1073
#: part/api.py:1074
msgid "Must be greater than zero"
msgstr ""
#: part/api.py:1077
#: part/api.py:1078
msgid "Must be a valid quantity"
msgstr ""
#: part/api.py:1092
#: part/api.py:1093
msgid "Specify location for initial part stock"
msgstr ""
#: part/api.py:1123 part/api.py:1127 part/api.py:1142 part/api.py:1146
#: part/api.py:1124 part/api.py:1128 part/api.py:1143 part/api.py:1147
msgid "This field is required"
msgstr ""
@ -5773,8 +5785,8 @@ msgstr ""
#: report/templates/report/inventree_test_report_base.html:79
#: stock/models.py:641 stock/templates/stock/item_base.html:322
#: templates/js/translated/build.js:420 templates/js/translated/build.js:572
#: templates/js/translated/build.js:1173 templates/js/translated/build.js:1670
#: templates/js/translated/build.js:421 templates/js/translated/build.js:573
#: templates/js/translated/build.js:1174 templates/js/translated/build.js:1671
#: templates/js/translated/model_renderers.js:118
#: templates/js/translated/order.js:115 templates/js/translated/order.js:3240
#: templates/js/translated/order.js:3329 templates/js/translated/stock.js:486
@ -6395,7 +6407,7 @@ msgid "Available Quantity"
msgstr ""
#: stock/templates/stock/item_base.html:394
#: templates/js/translated/build.js:1692
#: templates/js/translated/build.js:1693
msgid "No location set"
msgstr ""
@ -6808,7 +6820,7 @@ msgid "Plugin Settings"
msgstr ""
#: templates/InvenTree/settings/plugin.html:16
msgid "Changing the settings below require you to immediatly restart the server. Do not change this while under active usage."
msgid "Changing the settings below require you to immediately restart the server. Do not change this while under active usage."
msgstr ""
#: templates/InvenTree/settings/plugin.html:34
@ -7689,19 +7701,19 @@ msgstr ""
msgid "No attachments found"
msgstr ""
#: templates/js/translated/attachment.js:218
#: templates/js/translated/attachment.js:219
msgid "Edit Attachment"
msgstr ""
#: templates/js/translated/attachment.js:287
#: templates/js/translated/attachment.js:289
msgid "Upload Date"
msgstr ""
#: templates/js/translated/attachment.js:310
#: templates/js/translated/attachment.js:312
msgid "Edit attachment"
msgstr ""
#: templates/js/translated/attachment.js:319
#: templates/js/translated/attachment.js:321
msgid "Delete attachment"
msgstr ""
@ -7921,25 +7933,25 @@ msgstr ""
msgid "Substitutes Available"
msgstr ""
#: templates/js/translated/bom.js:832 templates/js/translated/build.js:1768
#: templates/js/translated/bom.js:832 templates/js/translated/build.js:1769
msgid "Variant stock allowed"
msgstr ""
#: templates/js/translated/bom.js:900 templates/js/translated/build.js:1813
#: templates/js/translated/bom.js:900 templates/js/translated/build.js:1814
#: templates/js/translated/order.js:3577
msgid "No Stock Available"
msgstr ""
#: templates/js/translated/bom.js:904 templates/js/translated/build.js:1817
#: templates/js/translated/bom.js:904 templates/js/translated/build.js:1818
msgid "Includes variant and substitute stock"
msgstr ""
#: templates/js/translated/bom.js:906 templates/js/translated/build.js:1819
#: templates/js/translated/bom.js:906 templates/js/translated/build.js:1820
#: templates/js/translated/part.js:759
msgid "Includes variant stock"
msgstr ""
#: templates/js/translated/bom.js:908 templates/js/translated/build.js:1821
#: templates/js/translated/bom.js:908 templates/js/translated/build.js:1822
msgid "Includes substitute stock"
msgstr ""
@ -7979,11 +7991,11 @@ msgstr ""
msgid "Delete BOM Item"
msgstr ""
#: templates/js/translated/bom.js:1158 templates/js/translated/build.js:1614
#: templates/js/translated/bom.js:1158 templates/js/translated/build.js:1615
msgid "No BOM items found"
msgstr ""
#: templates/js/translated/bom.js:1402 templates/js/translated/build.js:1752
#: templates/js/translated/bom.js:1402 templates/js/translated/build.js:1753
msgid "Required Part"
msgstr ""
@ -8015,256 +8027,256 @@ msgstr ""
msgid "There are incomplete outputs remaining for this build order"
msgstr ""
#: templates/js/translated/build.js:185
#: templates/js/translated/build.js:186
msgid "Build order is ready to be completed"
msgstr ""
#: templates/js/translated/build.js:190
#: templates/js/translated/build.js:191
msgid "Build Order is incomplete"
msgstr ""
#: templates/js/translated/build.js:218
#: templates/js/translated/build.js:219
msgid "Complete Build Order"
msgstr ""
#: templates/js/translated/build.js:259 templates/js/translated/stock.js:92
#: templates/js/translated/build.js:260 templates/js/translated/stock.js:92
#: templates/js/translated/stock.js:210
msgid "Next available serial number"
msgstr ""
#: templates/js/translated/build.js:261 templates/js/translated/stock.js:94
#: templates/js/translated/build.js:262 templates/js/translated/stock.js:94
#: templates/js/translated/stock.js:212
msgid "Latest serial number"
msgstr ""
#: templates/js/translated/build.js:270
#: templates/js/translated/build.js:271
msgid "The Bill of Materials contains trackable parts"
msgstr ""
#: templates/js/translated/build.js:271
#: templates/js/translated/build.js:272
msgid "Build outputs must be generated individually"
msgstr ""
#: templates/js/translated/build.js:279
#: templates/js/translated/build.js:280
msgid "Trackable parts can have serial numbers specified"
msgstr ""
#: templates/js/translated/build.js:280
#: templates/js/translated/build.js:281
msgid "Enter serial numbers to generate multiple single build outputs"
msgstr ""
#: templates/js/translated/build.js:287
#: templates/js/translated/build.js:288
msgid "Create Build Output"
msgstr ""
#: templates/js/translated/build.js:318
#: templates/js/translated/build.js:319
msgid "Allocate stock items to this build output"
msgstr ""
#: templates/js/translated/build.js:329
#: templates/js/translated/build.js:330
msgid "Unallocate stock from build output"
msgstr ""
#: templates/js/translated/build.js:338
#: templates/js/translated/build.js:339
msgid "Complete build output"
msgstr ""
#: templates/js/translated/build.js:346
#: templates/js/translated/build.js:347
msgid "Delete build output"
msgstr ""
#: templates/js/translated/build.js:369
#: templates/js/translated/build.js:370
msgid "Are you sure you wish to unallocate stock items from this build?"
msgstr ""
#: templates/js/translated/build.js:387
#: templates/js/translated/build.js:388
msgid "Unallocate Stock Items"
msgstr ""
#: templates/js/translated/build.js:407 templates/js/translated/build.js:559
#: templates/js/translated/build.js:408 templates/js/translated/build.js:560
msgid "Select Build Outputs"
msgstr ""
#: templates/js/translated/build.js:408 templates/js/translated/build.js:560
#: templates/js/translated/build.js:409 templates/js/translated/build.js:561
msgid "At least one build output must be selected"
msgstr ""
#: templates/js/translated/build.js:462 templates/js/translated/build.js:614
#: templates/js/translated/build.js:463 templates/js/translated/build.js:615
msgid "Output"
msgstr ""
#: templates/js/translated/build.js:480
#: templates/js/translated/build.js:481
msgid "Complete Build Outputs"
msgstr ""
#: templates/js/translated/build.js:627
#: templates/js/translated/build.js:628
msgid "Delete Build Outputs"
msgstr ""
#: templates/js/translated/build.js:716
#: templates/js/translated/build.js:717
msgid "No build order allocations found"
msgstr ""
#: templates/js/translated/build.js:754
#: templates/js/translated/build.js:755
msgid "Location not specified"
msgstr ""
#: templates/js/translated/build.js:1133
#: templates/js/translated/build.js:1134
msgid "No active build outputs found"
msgstr ""
#: templates/js/translated/build.js:1202
#: templates/js/translated/build.js:1203
msgid "Allocated Stock"
msgstr ""
#: templates/js/translated/build.js:1209
#: templates/js/translated/build.js:1210
msgid "No tracked BOM items for this build"
msgstr ""
#: templates/js/translated/build.js:1231
#: templates/js/translated/build.js:1232
msgid "Completed Tests"
msgstr ""
#: templates/js/translated/build.js:1236
#: templates/js/translated/build.js:1237
msgid "No required tests for this build"
msgstr ""
#: templates/js/translated/build.js:1709 templates/js/translated/build.js:2660
#: templates/js/translated/build.js:1710 templates/js/translated/build.js:2661
#: templates/js/translated/order.js:3277
msgid "Edit stock allocation"
msgstr ""
#: templates/js/translated/build.js:1711 templates/js/translated/build.js:2661
#: templates/js/translated/build.js:1712 templates/js/translated/build.js:2662
#: templates/js/translated/order.js:3278
msgid "Delete stock allocation"
msgstr ""
#: templates/js/translated/build.js:1729
#: templates/js/translated/build.js:1730
msgid "Edit Allocation"
msgstr ""
#: templates/js/translated/build.js:1739
#: templates/js/translated/build.js:1740
msgid "Remove Allocation"
msgstr ""
#: templates/js/translated/build.js:1764
#: templates/js/translated/build.js:1765
msgid "Substitute parts available"
msgstr ""
#: templates/js/translated/build.js:1781
#: templates/js/translated/build.js:1782
msgid "Quantity Per"
msgstr ""
#: templates/js/translated/build.js:1807 templates/js/translated/order.js:3584
#: templates/js/translated/build.js:1808 templates/js/translated/order.js:3584
msgid "Insufficient stock available"
msgstr ""
#: templates/js/translated/build.js:1809 templates/js/translated/order.js:3582
#: templates/js/translated/build.js:1810 templates/js/translated/order.js:3582
msgid "Sufficient stock available"
msgstr ""
#: templates/js/translated/build.js:1838 templates/js/translated/build.js:2083
#: templates/js/translated/build.js:2656 templates/js/translated/order.js:3596
#: templates/js/translated/build.js:1839 templates/js/translated/build.js:2084
#: templates/js/translated/build.js:2657 templates/js/translated/order.js:3596
msgid "Allocated"
msgstr ""
#: templates/js/translated/build.js:1886 templates/js/translated/order.js:3676
#: templates/js/translated/build.js:1887 templates/js/translated/order.js:3676
msgid "Build stock"
msgstr ""
#: templates/js/translated/build.js:1890 templates/stock_table.html:50
#: templates/js/translated/build.js:1891 templates/stock_table.html:50
msgid "Order stock"
msgstr ""
#: templates/js/translated/build.js:1893 templates/js/translated/order.js:3669
#: templates/js/translated/build.js:1894 templates/js/translated/order.js:3669
msgid "Allocate stock"
msgstr ""
#: templates/js/translated/build.js:1932 templates/js/translated/label.js:172
#: templates/js/translated/build.js:1933 templates/js/translated/label.js:172
#: templates/js/translated/order.js:756 templates/js/translated/order.js:2804
#: templates/js/translated/report.js:225
msgid "Select Parts"
msgstr ""
#: templates/js/translated/build.js:1933 templates/js/translated/order.js:2805
#: templates/js/translated/build.js:1934 templates/js/translated/order.js:2805
msgid "You must select at least one part to allocate"
msgstr ""
#: templates/js/translated/build.js:1982 templates/js/translated/order.js:2753
#: templates/js/translated/build.js:1983 templates/js/translated/order.js:2753
msgid "Specify stock allocation quantity"
msgstr ""
#: templates/js/translated/build.js:2056
#: templates/js/translated/build.js:2057
msgid "All Parts Allocated"
msgstr ""
#: templates/js/translated/build.js:2057
#: templates/js/translated/build.js:2058
msgid "All selected parts have been fully allocated"
msgstr ""
#: templates/js/translated/build.js:2071 templates/js/translated/order.js:2819
#: templates/js/translated/build.js:2072 templates/js/translated/order.js:2819
msgid "Select source location (leave blank to take from all locations)"
msgstr ""
#: templates/js/translated/build.js:2099
#: templates/js/translated/build.js:2100
msgid "Allocate Stock Items to Build Order"
msgstr ""
#: templates/js/translated/build.js:2110 templates/js/translated/order.js:2916
#: templates/js/translated/build.js:2111 templates/js/translated/order.js:2916
msgid "No matching stock locations"
msgstr ""
#: templates/js/translated/build.js:2182 templates/js/translated/order.js:2993
#: templates/js/translated/build.js:2183 templates/js/translated/order.js:2993
msgid "No matching stock items"
msgstr ""
#: templates/js/translated/build.js:2279
#: templates/js/translated/build.js:2280
msgid "Automatic Stock Allocation"
msgstr ""
#: templates/js/translated/build.js:2280
#: templates/js/translated/build.js:2281
msgid "Stock items will be automatically allocated to this build order, according to the provided guidelines"
msgstr ""
#: templates/js/translated/build.js:2282
#: templates/js/translated/build.js:2283
msgid "If a location is specifed, stock will only be allocated from that location"
msgstr ""
#: templates/js/translated/build.js:2283
#: templates/js/translated/build.js:2284
msgid "If stock is considered interchangeable, it will be allocated from the first location it is found"
msgstr ""
#: templates/js/translated/build.js:2284
#: templates/js/translated/build.js:2285
msgid "If substitute stock is allowed, it will be used where stock of the primary part cannot be found"
msgstr ""
#: templates/js/translated/build.js:2305
#: templates/js/translated/build.js:2306
msgid "Allocate Stock Items"
msgstr ""
#: templates/js/translated/build.js:2412
#: templates/js/translated/build.js:2413
msgid "No builds matching query"
msgstr ""
#: templates/js/translated/build.js:2447 templates/js/translated/part.js:1383
#: templates/js/translated/build.js:2448 templates/js/translated/part.js:1383
#: templates/js/translated/part.js:1850 templates/js/translated/stock.js:1682
#: templates/js/translated/stock.js:2340
msgid "Select"
msgstr ""
#: templates/js/translated/build.js:2467
#: templates/js/translated/build.js:2468
msgid "Build order is overdue"
msgstr ""
#: templates/js/translated/build.js:2495
#: templates/js/translated/build.js:2496
msgid "Progress"
msgstr ""
#: templates/js/translated/build.js:2531 templates/js/translated/stock.js:2582
#: templates/js/translated/build.js:2532 templates/js/translated/stock.js:2582
msgid "No user information"
msgstr ""
#: templates/js/translated/build.js:2637
#: templates/js/translated/build.js:2638
msgid "No parts allocated for"
msgstr ""
@ -10058,61 +10070,61 @@ msgstr ""
msgid "Select File Format"
msgstr ""
#: templates/js/translated/tables.js:534
#: templates/js/translated/tables.js:535
msgid "Loading data"
msgstr ""
#: templates/js/translated/tables.js:537
#: templates/js/translated/tables.js:538
msgid "rows per page"
msgstr ""
#: templates/js/translated/tables.js:542
#: templates/js/translated/tables.js:543
msgid "Showing all rows"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "Showing"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "to"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "of"
msgstr ""
#: templates/js/translated/tables.js:544
#: templates/js/translated/tables.js:545
msgid "rows"
msgstr ""
#: templates/js/translated/tables.js:548 templates/navbar.html:102
#: templates/js/translated/tables.js:549 templates/navbar.html:102
#: templates/search.html:8 templates/search_form.html:6
#: templates/search_form.html:7
msgid "Search"
msgstr ""
#: templates/js/translated/tables.js:551
#: templates/js/translated/tables.js:552
msgid "No matching results"
msgstr ""
#: templates/js/translated/tables.js:554
#: templates/js/translated/tables.js:555
msgid "Hide/Show pagination"
msgstr ""
#: templates/js/translated/tables.js:557
#: templates/js/translated/tables.js:558
msgid "Refresh"
msgstr ""
#: templates/js/translated/tables.js:560
#: templates/js/translated/tables.js:561
msgid "Toggle"
msgstr ""
#: templates/js/translated/tables.js:563
#: templates/js/translated/tables.js:564
msgid "Columns"
msgstr ""
#: templates/js/translated/tables.js:566
#: templates/js/translated/tables.js:567
msgid "All"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
- model: order.purchaseorder
pk: 1
fields:
reference: '0001'
reference: 'PO-0001'
description: "Ordering some screws"
supplier: 1
status: 10 # Pending
@ -13,7 +13,7 @@
- model: order.purchaseorder
pk: 2
fields:
reference: '0002'
reference: 'PO-0002'
description: "Ordering some more screws"
supplier: 3
status: 10 # Pending
@ -21,7 +21,7 @@
- model: order.purchaseorder
pk: 3
fields:
reference: '0003'
reference: 'PO-0003'
description: 'Another PO'
supplier: 3
status: 20 # Placed
@ -29,7 +29,7 @@
- model: order.purchaseorder
pk: 4
fields:
reference: '0004'
reference: 'PO-0004'
description: 'Another PO'
supplier: 3
status: 20 # Placed
@ -37,7 +37,7 @@
- model: order.purchaseorder
pk: 5
fields:
reference: '0005'
reference: 'PO-0005'
description: 'Another PO'
supplier: 3
status: 30 # Complete
@ -45,7 +45,7 @@
- model: order.purchaseorder
pk: 6
fields:
reference: '0006'
reference: 'PO-0006'
description: 'Another PO'
supplier: 3
status: 40 # Cancelled
@ -54,7 +54,7 @@
- model: order.purchaseorder
pk: 7
fields:
reference: '0007'
reference: 'PO-0007'
description: 'Another PO'
supplier: 2
status: 10 # Pending

View File

@ -1,7 +1,6 @@
# Generated by Django 3.2.4 on 2021-07-02 13:21
from django.db import migrations, models
import order.models
class Migration(migrations.Migration):
@ -14,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='purchaseorder',
name='reference',
field=models.CharField(default=order.models.get_next_po_number, help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'),
field=models.CharField(default="PO", help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'),
),
migrations.AlterField(
model_name='salesorder',
name='reference',
field=models.CharField(default=order.models.get_next_so_number, help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'),
field=models.CharField(default="SO", help_text='Order reference', max_length=64, unique=True, verbose_name='Reference'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.14 on 2022-07-07 11:55
from django.db import migrations, models
import order.validators
class Migration(migrations.Migration):
dependencies = [
('order', '0071_auto_20220628_0133'),
]
operations = [
migrations.AlterField(
model_name='salesorder',
name='reference',
field=models.CharField(default=order.validators.generate_next_sales_order_reference, help_text='Order reference', max_length=64, unique=True, validators=[order.validators.validate_sales_order_reference], verbose_name='Reference'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.14 on 2022-07-09 01:01
from django.db import migrations, models
import order.validators
class Migration(migrations.Migration):
dependencies = [
('order', '0072_alter_salesorder_reference'),
]
operations = [
migrations.AlterField(
model_name='purchaseorder',
name='reference',
field=models.CharField(default=order.validators.generate_next_purchase_order_reference, help_text='Order reference', max_length=64, unique=True, validators=[order.validators.validate_purchase_order_reference], verbose_name='Reference'),
),
]

View File

@ -0,0 +1,107 @@
# Generated by Django 3.2.14 on 2022-07-09 01:08
from django.db import migrations
def update_order_references(order_model, prefix):
"""Update all references of the given model, with the specified prefix"""
n = 0
for order in order_model.objects.all():
if not order.reference.startswith(prefix):
order.reference = prefix + order.reference
order.save()
n += 1
return n
def update_salesorder_reference(apps, schema_editor):
"""Migrate the reference pattern for the SalesOrder model"""
# Extract the existing "prefix" value
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
try:
prefix = InvenTreeSetting.objects.get(key='SALESORDER_REFERENCE_PREFIX').value
except Exception:
prefix = 'SO-'
# Construct a reference pattern
pattern = prefix + '{ref:04d}'
# Create or update the BuildOrder.reference pattern
try:
setting = InvenTreeSetting.objects.get(key='SALESORDER_REFERENCE_PATTERN')
setting.value = pattern
setting.save()
except InvenTreeSetting.DoesNotExist:
setting = InvenTreeSetting.objects.create(
key='SALESORDER_REFERENCE_PATTERN',
value=pattern,
)
# Update any existing sales order references
SalesOrder = apps.get_model('order', 'salesorder')
n = update_order_references(SalesOrder, prefix)
if n > 0:
print(f"Updated reference field for {n} SalesOrder objects")
def update_purchaseorder_reference(apps, schema_editor):
"""Migrate the reference pattern for the PurchaseOrder model"""
# Extract the existing "prefix" value
InvenTreeSetting = apps.get_model('common', 'inventreesetting')
try:
prefix = InvenTreeSetting.objects.get(key='PURCHASEORDER_REFERENCE_PREFIX').value
except Exception:
prefix = 'PO-'
# Construct a reference pattern
pattern = prefix + '{ref:04d}'
# Create or update the BuildOrder.reference pattern
try:
setting = InvenTreeSetting.objects.get(key='PURCHASEORDER_REFERENCE_PATTERN')
setting.value = pattern
setting.save()
except InvenTreeSetting.DoesNotExist:
setting = InvenTreeSetting.objects.create(
key='PURCHASEORDER_REFERENCE_PATTERN',
value=pattern,
)
# Update any existing sales order references
PurchaseOrder = apps.get_model('order', 'purchaseorder')
n = update_order_references(PurchaseOrder, prefix)
if n > 0:
print(f"Updated reference field for {n} PurchaseOrder objects")
def nop(apps, schema_editor):
"""Empty function for reverse migration"""
pass
class Migration(migrations.Migration):
dependencies = [
('order', '0073_alter_purchaseorder_reference'),
]
operations = [
migrations.RunPython(
update_salesorder_reference,
reverse_code=nop,
),
migrations.RunPython(
update_purchaseorder_reference,
reverse_code=nop,
)
]

View File

@ -24,14 +24,14 @@ from mptt.models import TreeForeignKey
import InvenTree.helpers
import InvenTree.ready
import order.validators
from common.notifications import InvenTreeNotificationBodies
from common.settings import currency_code_default
from company.models import Company, SupplierPart
from InvenTree.exceptions import log_error
from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeNotesField,
RoundingDecimalField)
from InvenTree.helpers import (decimal2string, getSetting, increment,
notify_responsible)
from InvenTree.helpers import decimal2string, getSetting, notify_responsible
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
from InvenTree.status_codes import (PurchaseOrderStatus, SalesOrderStatus,
StockHistoryCode, StockStatus)
@ -44,58 +44,6 @@ from users import models as UserModels
logger = logging.getLogger('inventree')
def get_next_po_number():
"""Returns the next available PurchaseOrder reference number."""
if PurchaseOrder.objects.count() == 0:
return '0001'
order = PurchaseOrder.objects.exclude(reference=None).last()
attempts = {order.reference}
reference = order.reference
while 1:
reference = increment(reference)
if reference in attempts:
# Escape infinite recursion
return reference
if PurchaseOrder.objects.filter(reference=reference).exists():
attempts.add(reference)
else:
break
return reference
def get_next_so_number():
"""Returns the next available SalesOrder reference number."""
if SalesOrder.objects.count() == 0:
return '0001'
order = SalesOrder.objects.exclude(reference=None).last()
attempts = {order.reference}
reference = order.reference
while 1:
reference = increment(reference)
if reference in attempts:
# Escape infinite recursion
return reference
if SalesOrder.objects.filter(reference=reference).exists():
attempts.add(reference)
else:
break
return reference
class Order(MetadataMixin, ReferenceIndexingMixin):
"""Abstract model for an order.
@ -119,7 +67,7 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
Ensures that the reference field is rebuilt whenever the instance is saved.
"""
self.rebuild_reference_field()
self.reference_int = self.rebuild_reference_field(self.reference)
if not self.creation_date:
self.creation_date = datetime.now().date()
@ -230,8 +178,21 @@ class PurchaseOrder(Order):
"""Return the API URL associated with the PurchaseOrder model"""
return reverse('api-po-list')
@classmethod
def api_defaults(cls, request):
"""Return default values for thsi model when issuing an API OPTIONS request"""
defaults = {
'reference': order.validators.generate_next_purchase_order_reference(),
}
return defaults
OVERDUE_FILTER = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'PURCHASEORDER_REFERENCE_PATTERN'
@staticmethod
def filterByDate(queryset, min_date, max_date):
"""Filter by 'minimum and maximum date range'.
@ -269,9 +230,8 @@ class PurchaseOrder(Order):
def __str__(self):
"""Render a string representation of this PurchaseOrder"""
prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX')
return f"{prefix}{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
return f"{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
reference = models.CharField(
unique=True,
@ -279,7 +239,10 @@ class PurchaseOrder(Order):
blank=False,
verbose_name=_('Reference'),
help_text=_('Order reference'),
default=get_next_po_number,
default=order.validators.generate_next_purchase_order_reference,
validators=[
order.validators.validate_purchase_order_reference,
]
)
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
@ -595,8 +558,20 @@ class SalesOrder(Order):
"""Return the API URL associated with the SalesOrder model"""
return reverse('api-so-list')
@classmethod
def api_defaults(cls, request):
"""Return default values for this model when issuing an API OPTIONS request"""
defaults = {
'reference': order.validators.generate_next_sales_order_reference(),
}
return defaults
OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'SALESORDER_REFERENCE_PATTERN'
@staticmethod
def filterByDate(queryset, min_date, max_date):
"""Filter by "minimum and maximum date range".
@ -634,9 +609,8 @@ class SalesOrder(Order):
def __str__(self):
"""Render a string representation of this SalesOrder"""
prefix = getSetting('SALESORDER_REFERENCE_PREFIX')
return f"{prefix}{self.reference} - {self.customer.name if self.customer else _('deleted')}"
return f"{self.reference} - {self.customer.name if self.customer else _('deleted')}"
def get_absolute_url(self):
"""Return the web URL for the detail view of this order"""
@ -648,7 +622,10 @@ class SalesOrder(Order):
blank=False,
verbose_name=_('Reference'),
help_text=_('Order reference'),
default=get_next_so_number,
default=order.validators.generate_next_sales_order_reference,
validators=[
order.validators.validate_sales_order_reference,
]
)
customer = models.ForeignKey(
@ -957,6 +934,7 @@ class OrderExtraLine(OrderLineItem):
max_digits=19,
decimal_places=4,
null=True, blank=True,
allow_negative=True,
verbose_name=_('Price'),
help_text=_('Unit price'),
)

View File

@ -23,8 +23,7 @@ from InvenTree.helpers import extract_serial_numbers, normalize
from InvenTree.serializers import (InvenTreeAttachmentSerializer,
InvenTreeDecimalField,
InvenTreeModelSerializer,
InvenTreeMoneySerializer,
ReferenceIndexingSerializerMixin)
InvenTreeMoneySerializer)
from InvenTree.status_codes import (PurchaseOrderStatus, SalesOrderStatus,
StockStatus)
from part.serializers import PartBriefSerializer
@ -86,7 +85,7 @@ class AbstractExtraLineMeta:
]
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
class PurchaseOrderSerializer(AbstractOrderSerializer, InvenTreeModelSerializer):
"""Serializer for a PurchaseOrder object."""
def __init__(self, *args, **kwargs):
@ -130,6 +129,14 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
reference = serializers.CharField(required=True)
def validate_reference(self, reference):
"""Custom validation for the reference field"""
# Ensure that the reference matches the required pattern
order.models.PurchaseOrder.validate_reference_field(reference)
return reference
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
class Meta:
@ -639,7 +646,7 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
]
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
class SalesOrderSerializer(AbstractOrderSerializer, InvenTreeModelSerializer):
"""Serializers for the SalesOrder object."""
def __init__(self, *args, **kwargs):
@ -683,6 +690,14 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
reference = serializers.CharField(required=True)
def validate_reference(self, reference):
"""Custom validation for the reference field"""
# Ensure that the reference matches the required pattern
order.models.SalesOrder.validate_reference_field(reference)
return reference
class Meta:
"""Metaclass options."""

View File

@ -82,7 +82,7 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
<td>{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
@ -222,7 +222,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-po-detail" order.pk %}', {
fields: {
reference: {
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
icon: 'fa-hashtag',
},
{% if order.lines.count == 0 and order.status == PurchaseOrderStatus.PENDING %}
supplier: {

View File

@ -78,7 +78,7 @@ src="{% static 'img/blank_image.png' %}"
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
<td>{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
@ -209,7 +209,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-so-detail" order.pk %}', {
fields: {
reference: {
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
icon: 'fa-hashtag',
},
{% if order.lines.count == 0 and order.status == SalesOrderStatus.PENDING %}
customer: {

View File

@ -94,23 +94,28 @@ class PurchaseOrderTest(OrderTest):
self.assertEqual(data['description'], 'Ordering some screws')
def test_po_reference(self):
"""Test that a reference with a too big / small reference is not possible."""
"""Test that a reference with a too big / small reference is handled correctly."""
# get permissions
self.assignRole('purchase_order.add')
url = reverse('api-po-list')
huge_number = 9223372036854775808
huge_number = "PO-92233720368547758089999999999999999"
self.post(
response = self.post(
url,
{
'supplier': 1,
'reference': huge_number,
'description': 'PO not created via the API',
'description': 'PO created via the API',
},
expected_code=201,
)
order = models.PurchaseOrder.objects.get(pk=response.data['pk'])
self.assertEqual(order.reference, 'PO-92233720368547758089999999999999999')
self.assertEqual(order.reference_int, 0x7fffffff)
def test_po_attachments(self):
"""Test the list endpoint for the PurchaseOrderAttachment model"""
url = reverse('api-po-attachment-list')
@ -149,7 +154,7 @@ class PurchaseOrderTest(OrderTest):
url,
{
'supplier': 1,
'reference': '123456789-xyz',
'reference': 'PO-123456789',
'description': 'PO created via the API',
},
expected_code=201
@ -177,19 +182,19 @@ class PurchaseOrderTest(OrderTest):
# Get detail info!
response = self.get(url)
self.assertEqual(response.data['pk'], pk)
self.assertEqual(response.data['reference'], '123456789-xyz')
self.assertEqual(response.data['reference'], 'PO-123456789')
# Try to alter (edit) the PurchaseOrder
response = self.patch(
url,
{
'reference': '12345-abc',
'reference': 'PO-12345',
},
expected_code=200
)
# Reference should have changed
self.assertEqual(response.data['reference'], '12345-abc')
self.assertEqual(response.data['reference'], 'PO-12345')
# Now, let's try to delete it!
# Initially, we do *not* have the required permission!
@ -213,7 +218,7 @@ class PurchaseOrderTest(OrderTest):
self.post(
reverse('api-po-list'),
{
'reference': '12345678',
'reference': 'PO-12345678',
'supplier': 1,
'description': 'A test purchase order',
},
@ -807,7 +812,7 @@ class SalesOrderTest(OrderTest):
url,
{
'customer': 4,
'reference': '12345',
'reference': 'SO-12345',
'description': 'Sales order',
},
expected_code=201
@ -824,7 +829,7 @@ class SalesOrderTest(OrderTest):
url,
{
'customer': 4,
'reference': '12345',
'reference': 'SO-12345',
'description': 'Another sales order',
},
expected_code=400
@ -834,19 +839,28 @@ class SalesOrderTest(OrderTest):
# Extract detail info for the SalesOrder
response = self.get(url)
self.assertEqual(response.data['reference'], '12345')
self.assertEqual(response.data['reference'], 'SO-12345')
# Try to alter (edit) the SalesOrder
# Initially try with an invalid reference field value
response = self.patch(
url,
{
'reference': '12345-a',
'reference': 'SO-12345-a',
},
expected_code=400
)
response = self.patch(
url,
{
'reference': 'SO-12346',
},
expected_code=200
)
# Reference should have changed
self.assertEqual(response.data['reference'], '12345-a')
self.assertEqual(response.data['reference'], 'SO-12346')
# Now, let's try to delete this SalesOrder
# Initially, we do not have the required permission
@ -866,14 +880,29 @@ class SalesOrderTest(OrderTest):
"""Test that we can create a new SalesOrder via the API."""
self.assignRole('sales_order.add')
self.post(
reverse('api-so-list'),
url = reverse('api-so-list')
# Will fail due to invalid reference field
response = self.post(
url,
{
'reference': '1234566778',
'customer': 4,
'description': 'A test sales order',
},
expected_code=201
expected_code=400,
)
self.assertIn('Reference must match required pattern', str(response.data['reference']))
self.post(
url,
{
'reference': 'SO-12345',
'customer': 4,
'description': 'A better test sales order',
},
expected_code=201,
)
def test_so_cancel(self):

View File

@ -40,19 +40,27 @@ class SalesOrderTest(TestCase):
# Create a SalesOrder to ship against
self.order = SalesOrder.objects.create(
customer=self.customer,
reference='1234',
reference='SO-1234',
customer_reference='ABC 55555'
)
# Create a Shipment against this SalesOrder
self.shipment = SalesOrderShipment.objects.create(
order=self.order,
reference='001',
reference='SO-001',
)
# Create a line item
self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part)
def test_so_reference(self):
"""Unit tests for sales order generation"""
# Test that a good reference is created when we have no existing orders
SalesOrder.objects.all().delete()
self.assertEqual(SalesOrder.generate_reference(), 'SO-0001')
def test_rebuild_reference(self):
"""Test that the 'reference_int' field gets rebuilt when the model is saved"""

View File

@ -35,15 +35,17 @@ class OrderTest(TestCase):
def test_basics(self):
"""Basic tests e.g. repr functions etc."""
order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
for pk in range(1, 8):
self.assertEqual(str(order), 'PO0001 - ACME')
order = PurchaseOrder.objects.get(pk=pk)
self.assertEqual(order.get_absolute_url(), f'/order/purchase-order/{pk}/')
self.assertEqual(order.reference, f'PO-{pk:04d}')
line = PurchaseOrderLineItem.objects.get(pk=1)
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO-0001 - ACME)")
def test_rebuild_reference(self):
"""Test that the reference_int field is correctly updated when the model is saved"""

View File

@ -0,0 +1,49 @@
"""Validation methods for the order app"""
def generate_next_sales_order_reference():
"""Generate the next available SalesOrder reference"""
from order.models import SalesOrder
return SalesOrder.generate_reference()
def generate_next_purchase_order_reference():
"""Generate the next available PurchasesOrder reference"""
from order.models import PurchaseOrder
return PurchaseOrder.generate_reference()
def validate_sales_order_reference_pattern(pattern):
"""Validate the SalesOrder reference 'pattern' setting"""
from order.models import SalesOrder
SalesOrder.validate_reference_pattern(pattern)
def validate_purchase_order_reference_pattern(pattern):
"""Validate the PurchaseOrder reference 'pattern' setting"""
from order.models import PurchaseOrder
PurchaseOrder.validate_reference_pattern(pattern)
def validate_sales_order_reference(value):
"""Validate that the SalesOrder reference field matches the required pattern"""
from order.models import SalesOrder
SalesOrder.validate_reference_field(value)
def validate_purchase_order_reference(value):
"""Validate that the PurchaseOrder reference field matches the required pattern"""
from order.models import PurchaseOrder
PurchaseOrder.validate_reference_field(value)

View File

@ -1545,6 +1545,20 @@ class BomFilter(rest_filters.FilterSet):
return queryset
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
def filter_on_order(self, queryset, name, value):
"""Filter the queryset based on whether each line item has any stock on order"""
value = str2bool(value)
if value:
queryset = queryset.filter(on_order__gt=0)
else:
queryset = queryset.filter(on_order=0)
return queryset
class BomList(ListCreateDestroyAPIView):
"""API endpoint for accessing a list of BomItem objects.

View File

@ -524,6 +524,8 @@ class BomItemSerializer(InvenTreeModelSerializer):
purchase_price_range = serializers.SerializerMethodField()
on_order = serializers.FloatField(read_only=True)
# Annotated fields for available stock
available_stock = serializers.FloatField(read_only=True)
available_substitute_stock = serializers.FloatField(read_only=True)
@ -593,6 +595,11 @@ class BomItemSerializer(InvenTreeModelSerializer):
ref = 'sub_part__'
# Annotate with the total "on order" amount for the sub-part
queryset = queryset.annotate(
on_order=part.filters.annotate_on_order_quantity(ref),
)
# Calculate "total stock" for the referenced sub_part
# Calculate the "build_order_allocations" for the sub_part
# Note that these fields are only aliased, not annotated
@ -719,6 +726,8 @@ class BomItemSerializer(InvenTreeModelSerializer):
'available_substitute_stock',
'available_variant_stock',
# Annotated field describing quantity on order
'on_order',
]

View File

@ -1514,6 +1514,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
part=Part.objects.get(pk=101),
quantity=10,
title='Making some assemblies',
reference='BO-9999',
status=BuildStatus.PRODUCTION,
)

View File

@ -6,6 +6,7 @@ import os
import pathlib
import warnings
from datetime import datetime
from importlib.metadata import metadata
from django.conf import settings
from django.db.utils import OperationalError, ProgrammingError
@ -195,11 +196,26 @@ class InvenTreePlugin(MixinBase, MetaBase):
self.define_package()
def _get_value(self, meta_name: str, package_name: str) -> str:
"""Extract values from class meta or package info.
Args:
meta_name (str): Name of the class meta to use.
package_name (str): Name of the package data to use.
Returns:
str: Extracted value, None if nothing found.
"""
val = getattr(self, meta_name, None)
if not val:
val = self.package.get(package_name, None)
return val
# region properties
@property
def description(self):
"""Description of plugin."""
description = getattr(self, 'DESCRIPTION', None)
description = self._get_value('DESCRIPTION', 'description')
if not description:
description = self.plugin_name()
return description
@ -207,9 +223,7 @@ class InvenTreePlugin(MixinBase, MetaBase):
@property
def author(self):
"""Author of plugin - either from plugin settings or git."""
author = getattr(self, 'AUTHOR', None)
if not author:
author = self.package.get('author')
author = self._get_value('AUTHOR', 'author')
if not author:
author = _('No author found') # pragma: no cover
return author
@ -229,19 +243,19 @@ class InvenTreePlugin(MixinBase, MetaBase):
@property
def version(self):
"""Version of plugin."""
version = getattr(self, 'VERSION', None)
version = self._get_value('VERSION', 'version')
return version
@property
def website(self):
"""Website of plugin - if set else None."""
website = getattr(self, 'WEBSITE', None)
website = self._get_value('WEBSITE', 'website')
return website
@property
def license(self):
"""License of plugin."""
lic = getattr(self, 'LICENSE', None)
lic = self._get_value('LICENSE', 'license')
return lic
# endregion
@ -273,9 +287,18 @@ class InvenTreePlugin(MixinBase, MetaBase):
"""Get last git commit for the plugin."""
return get_git_log(self.def_path)
def _get_package_metadata(self):
@classmethod
def _get_package_metadata(cls):
"""Get package metadata for plugin."""
return {} # pragma: no cover # TODO add usage for package metadata
meta = metadata(cls.__name__)
return {
'author': meta['Author-email'],
'description': meta['Summary'],
'version': meta['Version'],
'website': meta['Project-URL'],
'license': meta['License']
}
def define_package(self):
"""Add package info of the plugin into plugins context."""

View File

@ -208,6 +208,7 @@ class PluginsRegistry:
try:
plugin = entry.load()
plugin.is_package = True
plugin._get_package_metadata()
self.plugin_modules.append(plugin)
except Exception as error:
handle_error(error, do_raise=False, log_name='discovery')

View File

@ -425,7 +425,6 @@ class PurchaseOrderReport(ReportTemplateBase):
'order': order,
'reference': order.reference,
'supplier': order.supplier,
'prefix': common.models.InvenTreeSetting.get_setting('PURCHASEORDER_REFERENCE_PREFIX'),
'title': str(order),
}
@ -463,7 +462,6 @@ class SalesOrderReport(ReportTemplateBase):
'lines': order.lines,
'extra_lines': order.extra_lines,
'order': order,
'prefix': common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_PREFIX'),
'reference': order.reference,
'title': str(order),
}

View File

@ -30,8 +30,7 @@ import report.models
from company import models as CompanyModels
from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeNotesField,
InvenTreeURLField)
from InvenTree.models import InvenTreeAttachment, InvenTreeTree
from InvenTree.serializers import extract_int
from InvenTree.models import InvenTreeAttachment, InvenTreeTree, extract_int
from InvenTree.status_codes import StockHistoryCode, StockStatus
from part import models as PartModels
from plugin.events import trigger_event
@ -1708,8 +1707,7 @@ class StockItem(MetadataMixin, MPTTModel):
s += ' @ {loc}'.format(loc=self.location.name)
if self.purchase_order:
s += " ({pre}{po})".format(
pre=InvenTree.helpers.getSetting("PURCHASEORDER_REFERENCE_PREFIX"),
s += " ({po})".format(
po=self.purchase_order,
)

View File

@ -20,7 +20,8 @@ import InvenTree.serializers
import part.models as part_models
from common.settings import currency_code_default, currency_code_mappings
from company.serializers import SupplierPartSerializer
from InvenTree.serializers import InvenTreeDecimalField, extract_int
from InvenTree.models import extract_int
from InvenTree.serializers import InvenTreeDecimalField
from part.serializers import PartBriefSerializer
from .models import (StockItem, StockItemAttachment, StockItemTestResult,
@ -67,8 +68,8 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
def validate_serial(self, value):
"""Make sure serial is not to big."""
if extract_int(value) > 2147483647:
raise serializers.ValidationError('serial is to to big')
if abs(extract_int(value)) > 0x7fffffff:
raise serializers.ValidationError(_("Serial number is too large"))
return value

View File

@ -297,6 +297,7 @@
stock_item: {{ item.pk }},
}
},
multi_delete: true,
method: 'DELETE',
title: '{% trans "Delete Test Data" %}',
preFormContent: html,

View File

@ -87,7 +87,7 @@ class StockTest(InvenTreeTestCase):
# And there should be *no* items being build
self.assertEqual(part.quantity_being_built, 0)
build = Build.objects.create(reference='12345', part=part, title='A test build', quantity=1)
build = Build.objects.create(reference='BO-4444', part=part, title='A test build', quantity=1)
# Add some stock items which are "building"
for _ in range(10):
@ -395,13 +395,14 @@ class StockTest(InvenTreeTestCase):
item.serial = "-123"
item.save()
# Negative number should map to zero
self.assertEqual(item.serial_int, 0)
# Negative number should map to positive value
self.assertEqual(item.serial_int, 123)
# Test a very very large value
item.serial = '99999999999999999999999999999999999999999999999999999'
item.save()
# The 'integer' portion has been clipped to a maximum value
self.assertEqual(item.serial_int, 0x7fffffff)
# Non-numeric values should encode to zero

View File

@ -72,6 +72,7 @@ $('#history-delete').click(function() {
'{% url "api-notifications-list" %}',
{
method: 'DELETE',
multi_delete: true,
preFormContent: html,
title: '{% trans "Delete Notifications" %}',
onSuccess: function() {

View File

@ -12,8 +12,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PREFIX" %}
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_REGEX" %}
{% include "InvenTree/settings/setting.html" with key="BUILDORDER_REFERENCE_PATTERN" %}
</tbody>
</table>

View File

@ -18,7 +18,9 @@
{% include "InvenTree/settings/setting.html" with key="INVENTREE_RESTRICT_ABOUT" icon="fa-info-circle" %}
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
<tr><td colspan='5'></td></tr>
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
{% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %}
</tbody>
</table>

View File

@ -10,7 +10,7 @@
{% block content %}
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PREFIX" %}
{% include "InvenTree/settings/setting.html" with key="PURCHASEORDER_REFERENCE_PATTERN" %}
</tbody>
</table>
{% endblock %}

View File

@ -11,7 +11,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PREFIX" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_REFERENCE_PATTERN" %}
{% include "InvenTree/settings/setting.html" with key="SALESORDER_DEFAULT_SHIPMENT" icon="fa-truck-loading" %}
</tbody>
</table>

View File

@ -81,7 +81,7 @@
<td>
<span style="display: none;" id="about-copy-text">{% include "version.html" %}</span>
<span class="float-right">
<button class="btn clip-btn-version" type="button" data-bs-toggle='tooltip' title='{% trans "copy to clipboard" %}'><em class="fas fa-copy"></em> {% trans "copy version information" %}</button>
<button class="btn clip-btn" type="button" data-bs-toggle='tooltip' title='{% trans "copy to clipboard" %}'><em class="fas fa-copy"></em> {% trans "copy version information" %}</button>
</span>
</td>
</tr>

View File

@ -109,6 +109,7 @@ function deleteAttachments(attachments, url, options={}) {
constructForm(url, {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete Attachments" %}',
preFormContent: html,
form_data: {

View File

@ -292,8 +292,8 @@ function exportBom(part_id, options={}) {
choices: exportFormatOptions(),
},
cascade: {
label: '{% trans "Cascading" %}',
help_text: '{% trans "Download cascading / multi-level BOM" %}',
label: '{% trans "Multi Level BOM" %}',
help_text: '{% trans "Include BOM data for subassemblies" %}',
type: 'boolean',
value: inventreeLoad('bom-export-cascading', true),
},
@ -302,6 +302,7 @@ function exportBom(part_id, options={}) {
help_text: '{% trans "Select maximum number of BOM levels to export (0 = all levels)" %}',
type: 'integer',
value: 0,
required: true,
min_value: 0,
},
parameter_data: {
@ -651,9 +652,10 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
}
/*
* Delete the selected BOM items from the database
*/
function deleteBomItems(items, options={}) {
/* Delete the selected BOM items from the database
*/
function renderItem(item, opts={}) {
@ -696,6 +698,7 @@ function deleteBomItems(items, options={}) {
constructForm('{% url "api-bom-list" %}', {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete selected BOM items?" %}',
form_data: {
items: ids,
@ -877,6 +880,32 @@ function loadBomTable(table, options={}) {
return text;
},
footerFormatter: function(data) {
// Top-level BOM count
var top_total = 0;
// Total BOM count
var all_total = 0;
data.forEach(function(row) {
var q = +row['quantity'] || 0;
all_total += q;
if (row.part == options.parent_id) {
top_total += q;
}
});
var total = `${top_total}`;
if (top_total != all_total) {
total += ` / ${all_total}`;
}
return total;
}
});
cols.push({
@ -897,9 +926,10 @@ function loadBomTable(table, options={}) {
var text = `${available_stock}`;
if (available_stock <= 0) {
text = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock Available" %}</span>`;
text += `<span class='fas fa-times-circle icon-red float-right' title='{% trans "No Stock Available" %}'></span>`;
} else {
var extra = '';
if ((substitute_stock > 0) && (variant_stock > 0)) {
extra = '{% trans "Includes variant and substitute stock" %}';
} else if (variant_stock > 0) {
@ -913,6 +943,10 @@ function loadBomTable(table, options={}) {
}
}
if (row.on_order && row.on_order > 0) {
text += `<span class='fas fa-shopping-cart float-right' title='{% trans "On Order" %}: ${row.on_order}'></span>`;
}
return renderLink(text, url);
}
});
@ -1010,7 +1044,36 @@ function loadBomTable(table, options={}) {
can_build = available / row.quantity;
}
return formatDecimal(can_build, 2);
var text = formatDecimal(can_build, 2);
// Take "on order" quantity into account
if (row.on_order && row.on_order > 0 && row.quantity > 0) {
available += row.on_order;
can_build = available / row.quantity;
text += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Including On Order" %}: ${can_build}'></span>`;
}
return text;
},
footerFormatter: function(data) {
var can_build = null;
data.forEach(function(row) {
if (row.part == options.parent_id && row.quantity > 0) {
var cb = availableQuantity(row) / row.quantity;
if (can_build == null || cb < can_build) {
can_build = cb;
}
}
});
if (can_build == null) {
can_build = '-';
}
return can_build;
},
sorter: function(valA, valB, rowA, rowB) {
// Function to sort the "can build" quantity
@ -1131,6 +1194,7 @@ function loadBomTable(table, options={}) {
parentIdField: 'parentId',
treeShowField: 'sub_part',
showColumns: true,
showFooter: true,
name: 'bom',
sortable: true,
search: true,

View File

@ -4,7 +4,6 @@
/* globals
buildStatusDisplay,
constructForm,
global_settings,
imageHoverIcon,
inventreeGet,
launchModalForm,
@ -36,7 +35,7 @@
function buildFormFields() {
return {
reference: {
prefix: global_settings.BUILDORDER_REFERENCE_PREFIX,
icon: 'fa-hashtag',
},
part: {
filters: {
@ -731,9 +730,8 @@ function loadBuildOrderAllocationTable(table, options={}) {
switchable: false,
title: '{% trans "Build Order" %}',
formatter: function(value, row) {
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.build_detail.reference}`;
var ref = `${row.build_detail.reference}`;
return renderLink(ref, `/build/${row.build}/`);
}
@ -2372,7 +2370,6 @@ function loadBuildTable(table, options) {
filters,
{
success: function(response) {
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {
@ -2386,7 +2383,7 @@ function loadBuildTable(table, options) {
date = order.target_date;
}
var title = `${prefix}${order.reference}`;
var title = `${order.reference}`;
var color = '#4c68f5';
@ -2460,12 +2457,6 @@ function loadBuildTable(table, options) {
switchable: true,
formatter: function(value, row) {
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
}
var html = renderLink(value, '/build/' + row.pk + '/');
if (row.overdue) {

View File

@ -263,6 +263,7 @@ function deleteSupplierParts(parts, options={}) {
constructForm('{% url "api-supplier-part-list" %}', {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete Supplier Parts" %}',
preFormContent: html,
form_data: {
@ -491,6 +492,7 @@ function deleteManufacturerParts(selections, options={}) {
constructForm('{% url "api-manufacturer-part-list" %}', {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete Manufacturer Parts" %}',
preFormContent: html,
form_data: {
@ -538,6 +540,7 @@ function deleteManufacturerPartParameters(selections, options={}) {
constructForm('{% url "api-manufacturer-part-parameter-list" %}', {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete Parameters" %}',
preFormContent: html,
form_data: {

View File

@ -6,6 +6,7 @@
inventreeFormDataUpload,
inventreeGet,
inventreePut,
global_settings,
modalEnable,
modalShowSubmitButton,
renderBuild,
@ -250,30 +251,40 @@ function constructChangeForm(fields, options) {
*/
function constructDeleteForm(fields, options) {
// Request existing data from the API endpoint
// This data can be used to render some information on the form
$.ajax({
url: options.url,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
accepts: {
json: 'application/json',
},
success: function(data) {
// If we are deleting a specific "instance" (i.e. a single object)
// then we request the instance information first
// Store the instance data
options.instance = data;
// However we may be performing a "multi-delete" (against a list endpoint),
// in which case we do not want to perform such a request!
constructFormBody(fields, options);
},
error: function(xhr) {
// TODO: Handle error here
console.error(`Error in constructDeleteForm at '${options.url}`);
if (options.multi_delete) {
constructFormBody(fields, options);
} else {
// Request existing data from the API endpoint
// This data can be used to render some information on the form
$.ajax({
url: options.url,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
accepts: {
json: 'application/json',
},
success: function(data) {
showApiError(xhr, options.url);
}
});
// Store the instance data
options.instance = data;
constructFormBody(fields, options);
},
error: function(xhr) {
// TODO: Handle error here
console.error(`Error in constructDeleteForm at '${options.url}`);
showApiError(xhr, options.url);
}
});
}
}
@ -565,7 +576,7 @@ function constructFormBody(fields, options) {
$(modal).find('#modal-footer-buttons').html('');
// Insert "confirm" button (if required)
if (options.confirm) {
if (options.confirm && global_settings.INVENTREE_REQUIRE_CONFIRM) {
insertConfirmButton(options);
}

View File

@ -255,8 +255,7 @@ function renderOwner(name, data, parameters={}, options={}) {
// eslint-disable-next-line no-unused-vars
function renderPurchaseOrder(name, data, parameters={}, options={}) {
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
var html = `<span>${prefix}${data.reference}</span>`;
var html = `<span>${data.reference}</span>`;
var thumbnail = null;
@ -281,8 +280,7 @@ function renderPurchaseOrder(name, data, parameters={}, options={}) {
// eslint-disable-next-line no-unused-vars
function renderSalesOrder(name, data, parameters={}, options={}) {
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
var html = `<span>${prefix}${data.reference}</span>`;
var html = `<span>${data.reference}</span>`;
var thumbnail = null;
@ -307,10 +305,8 @@ function renderSalesOrder(name, data, parameters={}, options={}) {
// eslint-disable-next-line no-unused-vars
function renderSalesOrderShipment(name, data, parameters={}, options={}) {
var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
var html = `
<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>
<span>${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>
<span class='float-right'>
<small>{% trans "Shipment ID" %}: ${data.pk}</small>
</span>

View File

@ -431,7 +431,7 @@ function createSalesOrderShipment(options={}) {
var fields = salesOrderShipmentFields(options);
fields.reference.value = ref;
fields.reference.prefix = global_settings.SALESORDER_REFERENCE_PREFIX + options.reference;
fields.reference.prefix = options.reference;
constructForm('{% url "api-so-shipment-list" %}', {
method: 'POST',
@ -456,7 +456,7 @@ function createSalesOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
icon: 'fa-hashtag',
},
customer: {
value: options.customer,
@ -497,7 +497,7 @@ function createPurchaseOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
icon: 'fa-hashtag',
},
supplier: {
icon: 'fa-building',
@ -1081,9 +1081,7 @@ function newPurchaseOrderFromOrderWizard(e) {
},
{
success: function(response) {
var text = global_settings.PURCHASEORDER_REFERENCE_PREFIX || '';
text += response.reference;
var text = response.reference;
if (response.supplier_detail) {
text += ` ${response.supplier_detail.name}`;
@ -1545,8 +1543,6 @@ function loadPurchaseOrderTable(table, options) {
filters,
{
success: function(response) {
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {
var order = response[idx];
@ -1559,7 +1555,7 @@ function loadPurchaseOrderTable(table, options) {
date = order.target_date;
}
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
var title = `${order.reference} - ${order.supplier_detail.name}`;
var color = '#4c68f5';
@ -1623,12 +1619,6 @@ function loadPurchaseOrderTable(table, options) {
switchable: false,
formatter: function(value, row) {
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
}
var html = renderLink(value, `/order/purchase-order/${row.pk}/`);
if (row.overdue) {
@ -2336,8 +2326,6 @@ function loadSalesOrderTable(table, options) {
{
success: function(response) {
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {
var order = response[idx];
@ -2349,7 +2337,7 @@ function loadSalesOrderTable(table, options) {
date = order.target_date;
}
var title = `${prefix}${order.reference} - ${order.customer_detail.name}`;
var title = `${order.reference} - ${order.customer_detail.name}`;
// Default color is blue
var color = '#4c68f5';
@ -2435,13 +2423,6 @@ function loadSalesOrderTable(table, options) {
field: 'reference',
title: '{% trans "Sales Order" %}',
formatter: function(value, row) {
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
}
var html = renderLink(value, `/order/sales-order/${row.pk}/`);
if (row.overdue) {
@ -2891,7 +2872,7 @@ function allocateStockToSalesOrder(order_id, line_items, options={}) {
var fields = salesOrderShipmentFields(options);
fields.reference.value = ref;
fields.reference.prefix = global_settings.SALESORDER_REFERENCE_PREFIX + options.reference;
fields.reference.prefix = options.reference;
return fields;
}
@ -3123,9 +3104,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
title: '{% trans "Order" %}',
formatter: function(value, row) {
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.order_detail.reference}`;
var ref = `${row.order_detail.reference}`;
return renderLink(ref, `/order/sales-order/${row.order}/`);
}

View File

@ -974,9 +974,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
return '-';
}
var ref = global_settings.PURCHASEORDER_REFERENCE_PREFIX + order.reference;
var html = renderLink(ref, `/order/purchase-order/${order.pk}/`);
var html = renderLink(order.reference, `/order/purchase-order/${order.pk}/`);
html += purchaseOrderStatusDisplay(
order.status,

View File

@ -1916,10 +1916,7 @@ function loadStockTable(table, options) {
var text = `${row.purchase_order}`;
if (row.purchase_order_reference) {
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
text = prefix + row.purchase_order_reference;
text = row.purchase_order_reference;
}
return renderLink(text, link);

View File

@ -63,6 +63,10 @@ function getAvailableTableFilters(tableKey) {
type: 'bool',
title: '{% trans "Has Available Stock" %}',
},
on_order: {
type: 'bool',
title: '{% trans "On Order" %}',
},
validated: {
type: 'bool',
title: '{% trans "Validated" %}',

View File

@ -71,7 +71,7 @@ django-allauth==0.51.0
# via
# -r requirements.in
# django-allauth-2fa
django-allauth-2fa==0.9
django-allauth-2fa==0.10.0
# via -r requirements.in
django-cleanup==6.0.0
# via -r requirements.in