mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-25 04:23:33 +00:00
merge
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
name: Check Translations
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- l10
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- l10
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||||
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install gettext
|
||||||
|
pip3 install invoke
|
||||||
|
invoke install
|
||||||
|
- name: Test Translations
|
||||||
|
run: invoke translate
|
||||||
|
- name: Check Migration Files
|
||||||
|
run: python3 ci/check_migration_files.py
|
||||||
@@ -23,11 +23,13 @@ jobs:
|
|||||||
INVENTREE_MEDIA_ROOT: ./media
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
INVENTREE_STATIC_ROOT: ./static
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
steps:
|
steps:
|
||||||
- name: Install node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
- run: npm install
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Install node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: npm install
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
@@ -41,7 +43,6 @@ jobs:
|
|||||||
invoke static
|
invoke static
|
||||||
- name: Check HTML Files
|
- name: Check HTML Files
|
||||||
run: |
|
run: |
|
||||||
npm install markuplint
|
|
||||||
npx markuplint InvenTree/build/templates/build/*.html
|
npx markuplint InvenTree/build/templates/build/*.html
|
||||||
npx markuplint InvenTree/company/templates/company/*.html
|
npx markuplint InvenTree/company/templates/company/*.html
|
||||||
npx markuplint InvenTree/order/templates/order/*.html
|
npx markuplint InvenTree/order/templates/order/*.html
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ jobs:
|
|||||||
INVENTREE_MEDIA_ROOT: ./media
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
INVENTREE_STATIC_ROOT: ./static
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
steps:
|
steps:
|
||||||
- name: Install node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
- run: npm install
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Install node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: npm install
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
@@ -45,6 +47,5 @@ jobs:
|
|||||||
python check_js_templates.py
|
python check_js_templates.py
|
||||||
- name: Lint Javascript Files
|
- name: Lint Javascript Files
|
||||||
run: |
|
run: |
|
||||||
npm install eslint eslint-config-google
|
|
||||||
invoke render-js-files
|
invoke render-js-files
|
||||||
npx eslint js_tmp/*.js
|
npx eslint js_tmp/*.js
|
||||||
@@ -78,5 +78,4 @@ locale_stats.json
|
|||||||
|
|
||||||
# node.js
|
# node.js
|
||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -46,7 +46,7 @@ class InvenTreeAPITestCase(APITestCase):
|
|||||||
self.user.is_staff = True
|
self.user.is_staff = True
|
||||||
|
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
||||||
for role in self.roles:
|
for role in self.roles:
|
||||||
self.assignRole(role)
|
self.assignRole(role)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Pull rendered copies of the templated
|
Pull rendered copies of the templated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.http import response
|
from django.test import TestCase
|
||||||
from django.test import TestCase, testcases
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
|||||||
"""
|
"""
|
||||||
Custom MoneyField for clean migrations while using dynamic currency settings
|
Custom MoneyField for clean migrations while using dynamic currency settings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
# detect if creating migration
|
# detect if creating migration
|
||||||
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
|
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class InvenTreeOrderingFilter(OrderingFilter):
|
|||||||
ordering = []
|
ordering = []
|
||||||
|
|
||||||
for field in ordering_initial:
|
for field in ordering_initial:
|
||||||
|
|
||||||
reverse = field.startswith('-')
|
reverse = field.startswith('-')
|
||||||
|
|
||||||
if reverse:
|
if reverse:
|
||||||
@@ -52,7 +52,7 @@ class InvenTreeOrderingFilter(OrderingFilter):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Potentially, a single field could be "aliased" to multiple field,
|
Potentially, a single field could be "aliased" to multiple field,
|
||||||
|
|
||||||
(For example to enforce a particular ordering sequence)
|
(For example to enforce a particular ordering sequence)
|
||||||
|
|
||||||
e.g. to filter first by the integer value...
|
e.g. to filter first by the integer value...
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Command(BaseCommand):
|
|||||||
img = model.image
|
img = model.image
|
||||||
url = img.thumbnail.name
|
url = img.thumbnail.name
|
||||||
loc = os.path.join(settings.MEDIA_ROOT, url)
|
loc = os.path.join(settings.MEDIA_ROOT, url)
|
||||||
|
|
||||||
if not os.path.exists(loc):
|
if not os.path.exists(loc):
|
||||||
logger.info(f"Generating thumbnail image for '{img}'")
|
logger.info(f"Generating thumbnail image for '{img}'")
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def determine_metadata(self, request, view):
|
def determine_metadata(self, request, view):
|
||||||
|
|
||||||
self.request = request
|
self.request = request
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
Override get_serializer_info so that we can add 'default' values
|
Override get_serializer_info so that we can add 'default' values
|
||||||
to any fields whose Meta.model specifies a default value
|
to any fields whose Meta.model specifies a default value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_info = super().get_serializer_info(serializer)
|
serializer_info = super().get_serializer_info(serializer)
|
||||||
|
|
||||||
model_class = None
|
model_class = None
|
||||||
@@ -174,7 +174,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
# Extract extra information if an instance is available
|
# Extract extra information if an instance is available
|
||||||
if hasattr(serializer, 'instance'):
|
if hasattr(serializer, 'instance'):
|
||||||
instance = serializer.instance
|
instance = serializer.instance
|
||||||
|
|
||||||
if instance is None and model_class is not None:
|
if instance is None and model_class is not None:
|
||||||
# Attempt to find the instance based on kwargs lookup
|
# Attempt to find the instance based on kwargs lookup
|
||||||
kwargs = getattr(self.view, 'kwargs', None)
|
kwargs = getattr(self.view, 'kwargs', None)
|
||||||
@@ -240,7 +240,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
# Introspect writable related fields
|
# Introspect writable related fields
|
||||||
if field_info['type'] == 'field' and not field_info['read_only']:
|
if field_info['type'] == 'field' and not field_info['read_only']:
|
||||||
|
|
||||||
# If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset
|
# If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset
|
||||||
if isinstance(field, serializers.PrimaryKeyRelatedField):
|
if isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||||
model = field.queryset.model
|
model = field.queryset.model
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ from django.dispatch import receiver
|
|||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
from mptt.exceptions import InvalidMove
|
from mptt.exceptions import InvalidMove
|
||||||
|
|
||||||
from .validators import validate_tree_name
|
from InvenTree.fields import InvenTreeURLField
|
||||||
|
from InvenTree.validators import validate_tree_name
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
@@ -48,6 +49,9 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
A mixin for keeping track of numerical copies of the "reference" field.
|
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,
|
Here, we attempt to convert a "reference" field value (char) to an integer,
|
||||||
for performing fast natural sorting.
|
for performing fast natural sorting.
|
||||||
|
|
||||||
@@ -68,33 +72,39 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
|
|
||||||
reference = getattr(self, 'reference', '')
|
reference = getattr(self, 'reference', '')
|
||||||
|
|
||||||
# Default value if we cannot convert to an integer
|
self.reference_int = extract_int(reference)
|
||||||
ref_int = 0
|
|
||||||
|
|
||||||
# Look at the start of the string - can it be "integerized"?
|
reference_int = models.BigIntegerField(default=0)
|
||||||
result = re.match(r"^(\d+)", reference)
|
|
||||||
|
|
||||||
if result and len(result.groups()) == 1:
|
|
||||||
ref = result.groups()[0]
|
|
||||||
try:
|
|
||||||
ref_int = int(ref)
|
|
||||||
except:
|
|
||||||
ref_int = 0
|
|
||||||
|
|
||||||
self.reference_int = ref_int
|
def extract_int(reference):
|
||||||
|
# Default value if we cannot convert to an integer
|
||||||
|
ref_int = 0
|
||||||
|
|
||||||
reference_int = models.IntegerField(default=0)
|
# Look at the start of the string - can it be "integerized"?
|
||||||
|
result = re.match(r"^(\d+)", reference)
|
||||||
|
|
||||||
|
if result and len(result.groups()) == 1:
|
||||||
|
ref = result.groups()[0]
|
||||||
|
try:
|
||||||
|
ref_int = int(ref)
|
||||||
|
except:
|
||||||
|
ref_int = 0
|
||||||
|
return ref_int
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeAttachment(models.Model):
|
class InvenTreeAttachment(models.Model):
|
||||||
""" Provides an abstracted class for managing file attachments.
|
""" Provides an abstracted class for managing file attachments.
|
||||||
|
|
||||||
|
An attachment can be either an uploaded file, or an external URL
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
attachment: File
|
attachment: File
|
||||||
comment: String descriptor for the attachment
|
comment: String descriptor for the attachment
|
||||||
user: User associated with file upload
|
user: User associated with file upload
|
||||||
upload_date: Date the file was uploaded
|
upload_date: Date the file was uploaded
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def getSubdir(self):
|
def getSubdir(self):
|
||||||
"""
|
"""
|
||||||
Return the subdirectory under which attachments should be stored.
|
Return the subdirectory under which attachments should be stored.
|
||||||
@@ -103,11 +113,32 @@ class InvenTreeAttachment(models.Model):
|
|||||||
|
|
||||||
return "attachments"
|
return "attachments"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Either 'attachment' or 'link' must be specified!
|
||||||
|
if not self.attachment and not self.link:
|
||||||
|
raise ValidationError({
|
||||||
|
'attachment': _('Missing file'),
|
||||||
|
'link': _('Missing external link'),
|
||||||
|
})
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return os.path.basename(self.attachment.name)
|
if self.attachment is not None:
|
||||||
|
return os.path.basename(self.attachment.name)
|
||||||
|
else:
|
||||||
|
return str(self.link)
|
||||||
|
|
||||||
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
|
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
|
||||||
help_text=_('Select file to attach'))
|
help_text=_('Select file to attach'),
|
||||||
|
blank=True, null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
link = InvenTreeURLField(
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Link'),
|
||||||
|
help_text=_('Link to external URL')
|
||||||
|
)
|
||||||
|
|
||||||
comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment'))
|
comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment'))
|
||||||
|
|
||||||
@@ -123,7 +154,10 @@ class InvenTreeAttachment(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def basename(self):
|
def basename(self):
|
||||||
return os.path.basename(self.attachment.name)
|
if self.attachment:
|
||||||
|
return os.path.basename(self.attachment.name)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@basename.setter
|
@basename.setter
|
||||||
def basename(self, fn):
|
def basename(self, fn):
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
from djmoney.contrib.django_rest_framework.fields import MoneyField
|
from djmoney.contrib.django_rest_framework.fields import MoneyField
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
@@ -27,6 +28,8 @@ from rest_framework.fields import empty
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.serializers import DecimalField
|
from rest_framework.serializers import DecimalField
|
||||||
|
|
||||||
|
from .models import extract_int
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeMoneySerializer(MoneyField):
|
class InvenTreeMoneySerializer(MoneyField):
|
||||||
"""
|
"""
|
||||||
@@ -66,7 +69,7 @@ class InvenTreeMoneySerializer(MoneyField):
|
|||||||
|
|
||||||
if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty:
|
if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty:
|
||||||
return Money(amount, currency)
|
return Money(amount, currency)
|
||||||
|
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
@@ -239,20 +242,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
|
class ReferenceIndexingSerializerMixin():
|
||||||
"""
|
"""
|
||||||
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
|
This serializer mixin ensures the the reference is not to big / small
|
||||||
|
for the BigIntegerField
|
||||||
The only real addition here is that we support "renaming" of the attachment file.
|
|
||||||
"""
|
"""
|
||||||
|
def validate_reference(self, value):
|
||||||
# The 'filename' field must be present in the serializer
|
if extract_int(value) > models.BigIntegerField.MAX_BIGINT:
|
||||||
filename = serializers.CharField(
|
raise serializers.ValidationError('reference is to to big')
|
||||||
label=_('Filename'),
|
return value
|
||||||
required=False,
|
|
||||||
source='basename',
|
|
||||||
allow_blank=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeAttachmentSerializerField(serializers.FileField):
|
class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||||
@@ -284,6 +282,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
|
|||||||
return os.path.join(str(settings.MEDIA_URL), str(value))
|
return os.path.join(str(settings.MEDIA_URL), str(value))
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
|
||||||
|
"""
|
||||||
|
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
|
||||||
|
|
||||||
|
The only real addition here is that we support "renaming" of the attachment file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
attachment = InvenTreeAttachmentSerializerField(
|
||||||
|
required=False,
|
||||||
|
allow_null=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The 'filename' field must be present in the serializer
|
||||||
|
filename = serializers.CharField(
|
||||||
|
label=_('Filename'),
|
||||||
|
required=False,
|
||||||
|
source='basename',
|
||||||
|
allow_blank=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeImageSerializerField(serializers.ImageField):
|
class InvenTreeImageSerializerField(serializers.ImageField):
|
||||||
"""
|
"""
|
||||||
Custom image serializer.
|
Custom image serializer.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import moneyed
|
|||||||
import yaml
|
import yaml
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
|
import django.conf.locale
|
||||||
|
|
||||||
|
|
||||||
def _is_true(x):
|
def _is_true(x):
|
||||||
@@ -256,7 +257,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'user_sessions', # db user sessions
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
@@ -304,7 +305,7 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
MIDDLEWARE = CONFIG.get('middleware', [
|
MIDDLEWARE = CONFIG.get('middleware', [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'user_sessions.middleware.SessionMiddleware', # db user sessions
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
@@ -634,6 +635,12 @@ if _cache_host:
|
|||||||
# as well
|
# as well
|
||||||
Q_CLUSTER["django_redis"] = "worker"
|
Q_CLUSTER["django_redis"] = "worker"
|
||||||
|
|
||||||
|
# database user sessions
|
||||||
|
SESSION_ENGINE = 'user_sessions.backends.db'
|
||||||
|
LOGOUT_REDIRECT_URL = 'index'
|
||||||
|
SILENCED_SYSTEM_CHECKS = [
|
||||||
|
'admin.E410',
|
||||||
|
]
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
||||||
@@ -673,7 +680,7 @@ LANGUAGES = [
|
|||||||
('el', _('Greek')),
|
('el', _('Greek')),
|
||||||
('en', _('English')),
|
('en', _('English')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('es-mx', _('Spanish (Mexican')),
|
('es-mx', _('Spanish (Mexican)')),
|
||||||
('fr', _('French')),
|
('fr', _('French')),
|
||||||
('he', _('Hebrew')),
|
('he', _('Hebrew')),
|
||||||
('it', _('Italian')),
|
('it', _('Italian')),
|
||||||
@@ -691,6 +698,25 @@ LANGUAGES = [
|
|||||||
('zh-cn', _('Chinese')),
|
('zh-cn', _('Chinese')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Testing interface translations
|
||||||
|
if get_setting('TEST_TRANSLATIONS', False):
|
||||||
|
# Set default language
|
||||||
|
LANGUAGE_CODE = 'xx'
|
||||||
|
|
||||||
|
# Add to language catalog
|
||||||
|
LANGUAGES.append(('xx', 'Test'))
|
||||||
|
|
||||||
|
# Add custom languages not provided by Django
|
||||||
|
EXTRA_LANG_INFO = {
|
||||||
|
'xx': {
|
||||||
|
'code': 'xx',
|
||||||
|
'name': 'Test',
|
||||||
|
'name_local': 'Test'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
|
||||||
|
django.conf.locale.LANG_INFO = LANG_INFO
|
||||||
|
|
||||||
# Currencies available for use
|
# Currencies available for use
|
||||||
CURRENCIES = CONFIG.get(
|
CURRENCIES = CONFIG.get(
|
||||||
'currencies',
|
'currencies',
|
||||||
|
|||||||
@@ -781,6 +781,7 @@ input[type="submit"] {
|
|||||||
.btn-small {
|
.btn-small {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-remove {
|
.btn-remove {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
except NameError:
|
except NameError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Workers are not running: run it as synchronous task
|
# Workers are not running: run it as synchronous task
|
||||||
_func(*args, **kwargs)
|
_func(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from base64 import b64encode
|
|||||||
class HTMLAPITests(TestCase):
|
class HTMLAPITests(TestCase):
|
||||||
"""
|
"""
|
||||||
Test that we can access the REST API endpoints via the HTML interface.
|
Test that we can access the REST API endpoints via the HTML interface.
|
||||||
|
|
||||||
History: Discovered on 2021-06-28 a bug in InvenTreeModelSerializer,
|
History: Discovered on 2021-06-28 a bug in InvenTreeModelSerializer,
|
||||||
which raised an AssertionError when using the HTML API interface,
|
which raised an AssertionError when using the HTML API interface,
|
||||||
while the regular JSON interface continued to work as expected.
|
while the regular JSON interface continued to work as expected.
|
||||||
@@ -280,7 +280,7 @@ class APITests(InvenTreeAPITestCase):
|
|||||||
"""
|
"""
|
||||||
Tests for detail API endpoint actions
|
Tests for detail API endpoint actions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.basicAuth()
|
self.basicAuth()
|
||||||
|
|
||||||
url = reverse('api-part-detail', kwargs={'pk': 1})
|
url = reverse('api-part-detail', kwargs={'pk': 1})
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from rest_framework.documentation import include_docs_urls
|
|||||||
from .views import auth_request
|
from .views import auth_request
|
||||||
from .views import IndexView, SearchView, DatabaseStatsView
|
from .views import IndexView, SearchView, DatabaseStatsView
|
||||||
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
|
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
|
||||||
|
from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView
|
||||||
from .views import CurrencyRefreshView
|
from .views import CurrencyRefreshView
|
||||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||||
from .views import DynamicJsView
|
from .views import DynamicJsView
|
||||||
@@ -77,7 +78,7 @@ apipatterns = [
|
|||||||
settings_urls = [
|
settings_urls = [
|
||||||
|
|
||||||
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||||
|
|
||||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
||||||
|
|
||||||
@@ -157,6 +158,10 @@ frontendpatterns = [
|
|||||||
url(r'^search/', SearchView.as_view(), name='search'),
|
url(r'^search/', SearchView.as_view(), name='search'),
|
||||||
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||||
|
|
||||||
|
# DB user sessions
|
||||||
|
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
|
||||||
|
url(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ),
|
||||||
|
|
||||||
# Single Sign On / allauth
|
# Single Sign On / allauth
|
||||||
# overrides of urlpatterns
|
# overrides of urlpatterns
|
||||||
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
||||||
|
|||||||
@@ -12,11 +12,15 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 18
|
INVENTREE_API_VERSION = 19
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v19 -> 2021-12-02
|
||||||
|
- Adds the ability to filter the StockItem API by "part_tree"
|
||||||
|
- Returns only stock items which match a particular part.tree_id field
|
||||||
|
|
||||||
v18 -> 2021-11-15
|
v18 -> 2021-11-15
|
||||||
- Adds the ability to filter BomItem API by "uses" field
|
- Adds the ability to filter BomItem API by "uses" field
|
||||||
- This returns a list of all BomItems which "use" the specified part
|
- This returns a list of all BomItems which "use" the specified part
|
||||||
@@ -120,10 +124,10 @@ def isInvenTreeDevelopmentVersion():
|
|||||||
def inventreeDocsVersion():
|
def inventreeDocsVersion():
|
||||||
"""
|
"""
|
||||||
Return the version string matching the latest documentation.
|
Return the version string matching the latest documentation.
|
||||||
|
|
||||||
Development -> "latest"
|
Development -> "latest"
|
||||||
Release -> "major.minor.sub" e.g. "0.5.2"
|
Release -> "major.minor.sub" e.g. "0.5.2"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isInvenTreeDevelopmentVersion():
|
if isInvenTreeDevelopmentVersion():
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm
|
|||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from allauth.account.views import EmailView, PasswordResetFromKeyView
|
from allauth.account.views import EmailView, PasswordResetFromKeyView
|
||||||
from allauth.socialaccount.views import ConnectionsView
|
from allauth.socialaccount.views import ConnectionsView
|
||||||
|
from user_sessions.views import SessionDeleteView, SessionDeleteOtherView
|
||||||
|
|
||||||
from common.settings import currency_code_default, currency_codes
|
from common.settings import currency_code_default, currency_codes
|
||||||
|
|
||||||
@@ -733,6 +735,10 @@ class SettingsView(TemplateView):
|
|||||||
ctx["request"] = self.request
|
ctx["request"] = self.request
|
||||||
ctx['social_form'] = DisconnectForm(request=self.request)
|
ctx['social_form'] = DisconnectForm(request=self.request)
|
||||||
|
|
||||||
|
# user db sessions
|
||||||
|
ctx['session_key'] = self.request.session.session_key
|
||||||
|
ctx['session_list'] = self.request.user.session_set.filter(expire_date__gt=now()).order_by('-last_activity')
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView):
|
|||||||
success_url = reverse_lazy("account_login")
|
success_url = reverse_lazy("account_login")
|
||||||
|
|
||||||
|
|
||||||
|
class UserSessionOverride():
|
||||||
|
"""overrides sucessurl to lead to settings"""
|
||||||
|
def get_success_url(self):
|
||||||
|
return str(reverse_lazy('settings'))
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSessionDeleteView(UserSessionOverride, SessionDeleteView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CurrencyRefreshView(RedirectView):
|
class CurrencyRefreshView(RedirectView):
|
||||||
"""
|
"""
|
||||||
POST endpoint to refresh / update exchange rates
|
POST endpoint to refresh / update exchange rates
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ class BuildUnallocate(generics.CreateAPIView):
|
|||||||
queryset = Build.objects.none()
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
serializer_class = BuildUnallocationSerializer
|
serializer_class = BuildUnallocationSerializer
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@@ -231,7 +231,7 @@ class BuildComplete(generics.CreateAPIView):
|
|||||||
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
|||||||
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
|
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-11-28 01:51
|
||||||
|
|
||||||
|
import InvenTree.fields
|
||||||
|
import InvenTree.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('build', '0032_auto_20211014_0632'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='buildorderattachment',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='buildorderattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-12-01 21:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('build', '0033_auto_20211128_0151'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='build',
|
||||||
|
name='reference_int',
|
||||||
|
field=models.BigIntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -66,7 +66,7 @@ def get_next_build_number():
|
|||||||
attempts.add(reference)
|
attempts.add(reference)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
return reference
|
return reference
|
||||||
|
|
||||||
|
|
||||||
@@ -94,13 +94,13 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-build-list')
|
return reverse('api-build-list')
|
||||||
|
|
||||||
def api_instance_filters(self):
|
def api_instance_filters(self):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'parent': {
|
'parent': {
|
||||||
'exclude_tree': self.pk,
|
'exclude_tree': self.pk,
|
||||||
@@ -1178,7 +1178,7 @@ class BuildItem(models.Model):
|
|||||||
bom_item = PartModels.BomItem.objects.get(part=self.build.part, sub_part=ancestor)
|
bom_item = PartModels.BomItem.objects.get(part=self.build.part, sub_part=ancestor)
|
||||||
except PartModels.BomItem.DoesNotExist:
|
except PartModels.BomItem.DoesNotExist:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# A matching BOM item has been found!
|
# A matching BOM item has been found!
|
||||||
if idx == 0 or bom_item.allow_variants:
|
if idx == 0 or bom_item.allow_variants:
|
||||||
bom_item_valid = True
|
bom_item_valid = True
|
||||||
@@ -1234,7 +1234,7 @@ class BuildItem(models.Model):
|
|||||||
thumb_url = self.stock_item.part.image.thumbnail.url
|
thumb_url = self.stock_item.part.image.thumbnail.url
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if thumb_url is None and self.bom_item and self.bom_item.sub_part:
|
if thumb_url is None and self.bom_item and self.bom_item.sub_part:
|
||||||
try:
|
try:
|
||||||
thumb_url = self.bom_item.sub_part.image.thumbnail.url
|
thumb_url = self.bom_item.sub_part.image.thumbnail.url
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from rest_framework import serializers
|
|||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
|
||||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
|
from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
from InvenTree.serializers import InvenTreeDecimalField
|
from InvenTree.serializers import InvenTreeDecimalField
|
||||||
@@ -32,7 +32,7 @@ from users.serializers import OwnerSerializer
|
|||||||
from .models import Build, BuildItem, BuildOrderAttachment
|
from .models import Build, BuildItem, BuildOrderAttachment
|
||||||
|
|
||||||
|
|
||||||
class BuildSerializer(InvenTreeModelSerializer):
|
class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializes a Build object
|
Serializes a Build object
|
||||||
"""
|
"""
|
||||||
@@ -309,7 +309,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_bom_item(self, bom_item):
|
def validate_bom_item(self, bom_item):
|
||||||
|
|
||||||
# TODO: Fix this validation - allow for variants and substitutes!
|
# TODO: Fix this validation - allow for variants and substitutes!
|
||||||
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
@@ -332,7 +332,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
if not stock_item.in_stock:
|
if not stock_item.in_stock:
|
||||||
raise ValidationError(_("Item must be in stock"))
|
raise ValidationError(_("Item must be in stock"))
|
||||||
|
|
||||||
return stock_item
|
return stock_item
|
||||||
|
|
||||||
quantity = serializers.DecimalField(
|
quantity = serializers.DecimalField(
|
||||||
@@ -398,7 +398,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
# Output *cannot* be set for un-tracked parts
|
# Output *cannot* be set for un-tracked parts
|
||||||
if output is not None and not bom_item.sub_part.trackable:
|
if output is not None and not bom_item.sub_part.trackable:
|
||||||
|
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'output': _('Build output cannot be specified for allocation of untracked parts')
|
'output': _('Build output cannot be specified for allocation of untracked parts')
|
||||||
})
|
})
|
||||||
@@ -422,14 +422,14 @@ class BuildAllocationSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
Validation
|
Validation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().validate(data)
|
super().validate(data)
|
||||||
|
|
||||||
items = data.get('items', [])
|
items = data.get('items', [])
|
||||||
|
|
||||||
if len(items) == 0:
|
if len(items) == 0:
|
||||||
raise ValidationError(_('Allocation items must be provided'))
|
raise ValidationError(_('Allocation items must be provided'))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
Serializer for a BuildAttachment
|
Serializer for a BuildAttachment
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BuildOrderAttachment
|
model = BuildOrderAttachment
|
||||||
|
|
||||||
@@ -525,6 +523,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'build',
|
'build',
|
||||||
'attachment',
|
'attachment',
|
||||||
|
'link',
|
||||||
'filename',
|
'filename',
|
||||||
'comment',
|
'comment',
|
||||||
'upload_date',
|
'upload_date',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
<li class='breadcrumb-item'><a href='{% url "build-index" %}'>{% trans "Build Orders" %}</a></li>
|
<li class='breadcrumb-item'><a href='{% url "build-index" %}'>{% trans "Build Orders" %}</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "build-detail" build.id %}'>{{ build }}</a></li>
|
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "build-detail" build.id %}'>{{ build }}</a></li>
|
||||||
{% endblock %}
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<img class="part-thumb"
|
<img class="part-thumb"
|
||||||
@@ -21,7 +21,7 @@ src="{{ build.part.image.url }}"
|
|||||||
{% else %}
|
{% else %}
|
||||||
src="{% static 'img/blank_image.png' %}"
|
src="{% static 'img/blank_image.png' %}"
|
||||||
{% endif %}/>
|
{% endif %}/>
|
||||||
{% endblock %}
|
{% endblock thumbnail %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Build Order" %} {{ build }}
|
{% trans "Build Order" %} {{ build }}
|
||||||
@@ -66,11 +66,23 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
<p>{{ build.title }}</p>
|
<table class='table table-striped table-condensed'>
|
||||||
|
<col width='25'>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-shapes'></span></td>
|
||||||
|
<td>{% trans "Part" %}</td>
|
||||||
|
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-info-circle'></span></td>
|
||||||
|
<td>{% trans "Build Description" %}</td>
|
||||||
|
<td>{{ build.title }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<div class='info-messages'>
|
<div class='info-messages'>
|
||||||
{% if build.sales_order %}
|
{% if build.sales_order %}
|
||||||
@@ -114,11 +126,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tr>
|
<col width='25'>
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
|
||||||
<td>{% trans "Part" %}</td>
|
|
||||||
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{% trans "Quantity" %}</td>
|
<td>{% trans "Quantity" %}</td>
|
||||||
|
|||||||
@@ -431,53 +431,17 @@ enableDragAndDrop(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Callback for creating a new attachment
|
loadAttachmentTable('{% url "api-build-attachment-list" %}', {
|
||||||
$('#new-attachment').click(function() {
|
filters: {
|
||||||
|
build: {{ build.pk }},
|
||||||
constructForm('{% url "api-build-attachment-list" %}', {
|
},
|
||||||
fields: {
|
fields: {
|
||||||
attachment: {},
|
build: {
|
||||||
comment: {},
|
value: {{ build.pk }},
|
||||||
build: {
|
hidden: true,
|
||||||
value: {{ build.pk }},
|
|
||||||
hidden: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
title: '{% trans "Add Attachment" %}',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
loadAttachmentTable(
|
|
||||||
'{% url "api-build-attachment-list" %}',
|
|
||||||
{
|
|
||||||
filters: {
|
|
||||||
build: {{ build.pk }},
|
|
||||||
},
|
|
||||||
onEdit: function(pk) {
|
|
||||||
var url = `/api/build/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
|
||||||
filename: {},
|
|
||||||
comment: {},
|
|
||||||
},
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
title: '{% trans "Edit Attachment" %}',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDelete: function(pk) {
|
|
||||||
|
|
||||||
constructForm(`/api/build/attachment/${pk}/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
|
||||||
title: '{% trans "Delete Attachment" %}',
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
$('#edit-notes').click(function() {
|
$('#edit-notes').click(function() {
|
||||||
constructForm('{% url "api-build-detail" build.pk %}', {
|
constructForm('{% url "api-build-detail" build.pk %}', {
|
||||||
|
|||||||
@@ -2,14 +2,21 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='details' text="Build Order Details" icon="fa-info-circle" %}
|
{% trans "Build Order Details" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %}
|
||||||
{% if build.active %}
|
{% if build.active %}
|
||||||
{% include "sidebar_item.html" with label='allocate' text="Allocate Stock" icon="fa-tasks" %}
|
{% trans "Allocate Stock" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not build.is_complete %}
|
{% if not build.is_complete %}
|
||||||
{% include "sidebar_item.html" with label='outputs' text="Pending Items" icon="fa-tools" %}
|
{% trans "Pending Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label='completed' text="Completed Items" icon="fa-boxes" %}
|
{% trans "Completed Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='children' text="Child Build Orders" icon="fa-sitemap" %}
|
{% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
|
||||||
{% include "sidebar_item.html" with label='attachments' text="Attachments" icon="fa-paperclip" %}
|
{% trans "Child Build Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label='notes' text="Notes" icon="fa-clipboard" %}
|
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
|
||||||
|
{% trans "Attachments" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='notes' text=text icon="fa-clipboard" %}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
|
|||||||
permission_classes = [
|
permission_classes = [
|
||||||
GlobalSettingsPermissions,
|
GlobalSettingsPermissions,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsList(SettingsList):
|
class UserSettingsList(SettingsList):
|
||||||
"""
|
"""
|
||||||
@@ -124,7 +124,7 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
|||||||
|
|
||||||
queryset = common.models.InvenTreeUserSetting.objects.all()
|
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||||
serializer_class = common.serializers.UserSettingsSerializer
|
serializer_class = common.serializers.UserSettingsSerializer
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
UserSettingsPermissions,
|
UserSettingsPermissions,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class CommonConfig(AppConfig):
|
|||||||
name = 'common'
|
name = 'common'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
|
||||||
self.clear_restart_flag()
|
self.clear_restart_flag()
|
||||||
|
|
||||||
def clear_restart_flag(self):
|
def clear_restart_flag(self):
|
||||||
@@ -22,7 +22,7 @@ class CommonConfig(AppConfig):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'):
|
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'):
|
||||||
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
|
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
|
||||||
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
|
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
for key, value in settings.items():
|
for key, value in settings.items():
|
||||||
validator = cls.get_setting_validator(key)
|
validator = cls.get_setting_validator(key)
|
||||||
|
|
||||||
if cls.validator_is_bool(validator):
|
if cls.is_protected(key):
|
||||||
|
value = '***'
|
||||||
|
elif cls.validator_is_bool(validator):
|
||||||
value = InvenTree.helpers.str2bool(value)
|
value = InvenTree.helpers.str2bool(value)
|
||||||
elif cls.validator_is_int(validator):
|
elif cls.validator_is_int(validator):
|
||||||
try:
|
try:
|
||||||
@@ -485,7 +487,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
elif self.is_int():
|
elif self.is_int():
|
||||||
return 'integer'
|
return 'integer'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return 'string'
|
return 'string'
|
||||||
|
|
||||||
@@ -538,6 +540,19 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_protected(cls, key):
|
||||||
|
"""
|
||||||
|
Check if the setting value is protected
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = str(key).strip().upper()
|
||||||
|
|
||||||
|
if key in cls.GLOBAL_SETTINGS:
|
||||||
|
return cls.GLOBAL_SETTINGS[key].get('protected', False)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def settings_group_options():
|
def settings_group_options():
|
||||||
"""build up group tuple for settings based on gour choices"""
|
"""build up group tuple for settings based on gour choices"""
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def get_value(self, obj):
|
||||||
|
"""
|
||||||
|
Make sure protected values are not returned
|
||||||
|
"""
|
||||||
|
result = obj.value
|
||||||
|
|
||||||
|
# never return protected values
|
||||||
|
if obj.is_protected:
|
||||||
|
result = '***'
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsSerializer(SettingsSerializer):
|
class GlobalSettingsSerializer(SettingsSerializer):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ class ManufacturerPartParameterList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = ManufacturerPartParameter.objects.all()
|
queryset = ManufacturerPartParameter.objects.all()
|
||||||
serializer_class = ManufacturerPartParameterSerializer
|
serializer_class = ManufacturerPartParameterSerializer
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
|
||||||
# Do we wish to include any extra detail?
|
# Do we wish to include any extra detail?
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ class SupplierPart(models.Model):
|
|||||||
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
def api_instance_filters(self):
|
def api_instance_filters(self):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'manufacturer_part': {
|
'manufacturer_part': {
|
||||||
'part': self.part.pk
|
'part': self.part.pk
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
supplier_detail = kwargs.pop('supplier_detail', True)
|
supplier_detail = kwargs.pop('supplier_detail', True)
|
||||||
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
||||||
|
|
||||||
prettify = kwargs.pop('pretty', False)
|
prettify = kwargs.pop('pretty', False)
|
||||||
|
|
||||||
super(SupplierPartSerializer, self).__init__(*args, **kwargs)
|
super(SupplierPartSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -19,21 +19,26 @@
|
|||||||
{% include "admin_button.html" with url=url %}
|
{% include "admin_button.html" with url=url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if company.is_supplier and roles.purchase_order.add %}
|
{% if company.is_supplier and roles.purchase_order.add %}
|
||||||
<button type='button' class='btn btn-outline-secondary' id='company-order-2' title='{% trans "Create Purchase Order" %}'>
|
<button type='button' class='btn btn-outline-primary' id='company-order-2' title='{% trans "Create Purchase Order" %}'>
|
||||||
<span class='fas fa-shopping-cart'/>
|
<span class='fas fa-shopping-cart'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.company.change_company %}
|
<button id='company-edit-actions' title='{% trans "Company actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
|
||||||
<button type='button' class='btn btn-outline-secondary' id='company-edit' title='{% trans "Edit company information" %}'>
|
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||||
<span class='fas fa-edit icon-green'/>
|
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
<ul class='dropdown-menu' role='menu'>
|
||||||
{% if perms.company.delete_company %}
|
{% if perms.company.change_company %}
|
||||||
<button type='button' class='btn btn-outline-secondary' id='company-delete' title='{% trans "Delete Company" %}'>
|
<li><a class='dropdown-item' href='#' id='company-edit' title='{% trans "Edit company information" %}'>
|
||||||
<span class='fas fa-trash-alt icon-red'/>
|
<span class='fas fa-edit icon-green'></span> {% trans "Edit Company" %}
|
||||||
</button>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% if perms.company.delete_company %}
|
||||||
|
<li><a class='dropdown-item' href='#' id='company-delete' title='{% trans "Delete company" %}'>
|
||||||
|
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Company" %}
|
||||||
|
</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<div class='dropzone part-thumb-container' id='company-thumb'>
|
<div class='dropzone part-thumb-container' id='company-thumb'>
|
||||||
@@ -56,7 +61,29 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
<p>{{ company.description }}</p>
|
<table class='table table-striped table-condensed'>
|
||||||
|
<col width='25'>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-info-circle'></span></td>
|
||||||
|
<td>{% trans "Description" %}</td>
|
||||||
|
<td>{{ company.description }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-industry'></span></td>
|
||||||
|
<td>{%trans "Manufacturer" %}</td>
|
||||||
|
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-building'></span></td>
|
||||||
|
<td>{% trans "Supplier" %}</td>
|
||||||
|
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-user-tie'></span></td>
|
||||||
|
<td>{% trans "Customer" %}</td>
|
||||||
|
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -110,22 +137,6 @@
|
|||||||
<td>{{ company.contact }}{% include "clip.html"%}</td>
|
<td>{{ company.contact }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-industry'></span></td>
|
|
||||||
<td>{%trans "Manufacturer" %}</td>
|
|
||||||
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-building'></span></td>
|
|
||||||
<td>{% trans "Supplier" %}</td>
|
|
||||||
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-user-tie'></span></td>
|
|
||||||
<td>{% trans "Customer" %}</td>
|
|
||||||
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ InvenTree | {% trans "Manufacturer Part" %}
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% include "company/manufacturer_part_sidebar.html" %}
|
{% include "company/manufacturer_part_sidebar.html" %}
|
||||||
{% endblock %}
|
{% endblock sidebar %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
<li class='breadcrumb-item'><a href='{% url "manufacturer-index" %}'>{% trans "Manufacturers" %}</a></li>
|
<li class='breadcrumb-item'><a href='{% url "manufacturer-index" %}'>{% trans "Manufacturers" %}</a></li>
|
||||||
@@ -16,13 +16,13 @@ InvenTree | {% trans "Manufacturer Part" %}
|
|||||||
<li class='breadcrumb-item'><a href='{% url "company-detail" part.manufacturer.id %}'>{{ part.manufacturer.name }}</a></li>
|
<li class='breadcrumb-item'><a href='{% url "company-detail" part.manufacturer.id %}'>{{ part.manufacturer.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "manufacturer-part-detail" part.id %}'>{{ part.MPN }}</a></li>
|
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "manufacturer-part-detail" part.id %}'>{{ part.MPN }}</a></li>
|
||||||
{% endblock %}
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
<h4>
|
<h4>
|
||||||
{% trans "Manufacturer Part" %}: {{ part.part.full_name }}
|
{% trans "Manufacturer Part" %}: {{ part.part.full_name }}
|
||||||
</h4>
|
</h4>
|
||||||
{% endblock %}
|
{% endblock heading %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
{% if user.is_staff and perms.company.change_company %}
|
{% if user.is_staff and perms.company.change_company %}
|
||||||
@@ -46,7 +46,7 @@ InvenTree | {% trans "Manufacturer Part" %}
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<img class='part-thumb'
|
<img class='part-thumb'
|
||||||
@@ -55,50 +55,56 @@ src='{{ part.part.image.url }}'
|
|||||||
{% else %}
|
{% else %}
|
||||||
src="{% static 'img/blank_image.png' %}"
|
src="{% static 'img/blank_image.png' %}"
|
||||||
{% endif %}/>
|
{% endif %}/>
|
||||||
{% endblock %}
|
{% endblock thumbnail %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
{% endblock %}
|
<table class='table table-striped table-condensed'>
|
||||||
|
<col width='25'>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-shapes'></span></td>
|
||||||
|
<td>{% trans "Internal Part" %}</td>
|
||||||
|
<td>
|
||||||
|
{% if part.part %}
|
||||||
|
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if part.description %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{% trans "Description" %}</td>
|
||||||
|
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock details %}
|
||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
|
|
||||||
<table class="table table-striped table-condensed">
|
<table class="table table-striped table-condensed">
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
<tr>
|
||||||
<td>{% trans "Internal Part" %}</td>
|
<td><span class='fas fa-industry'></span></td>
|
||||||
<td>
|
<td>{% trans "Manufacturer" %}</td>
|
||||||
{% if part.part %}
|
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td>
|
||||||
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
|
</tr>
|
||||||
{% endif %}
|
<tr>
|
||||||
</td>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
</tr>
|
<td>{% trans "MPN" %}</td>
|
||||||
{% if part.description %}
|
<td>{{ part.MPN }}{% include "clip.html"%}</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td></td>
|
{% if part.link %}
|
||||||
<td>{% trans "Description" %}</td>
|
<tr>
|
||||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
<td><span class='fas fa-link'></span></td>
|
||||||
</tr>
|
<td>{% trans "External Link" %}</td>
|
||||||
{% endif %}
|
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
||||||
{% if part.link %}
|
</tr>
|
||||||
<tr>
|
{% endif %}
|
||||||
<td><span class='fas fa-link'></span></td>
|
|
||||||
<td>{% trans "External Link" %}</td>
|
|
||||||
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-industry'></span></td>
|
|
||||||
<td>{% trans "Manufacturer" %}</td>
|
|
||||||
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td></tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
|
||||||
<td>{% trans "MPN" %}</td>
|
|
||||||
<td>{{ part.MPN }}{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock details_right %}
|
||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='parameters' text="Parameters" icon="fa-th-list" %}
|
{% trans "Parameters" as text %}
|
||||||
{% include "sidebar_item.html" with label='supplier-parts' text="Supplier Parts" icon="fa-building" %}
|
{% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %}
|
||||||
|
{% trans "Supplier Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
|
||||||
@@ -3,17 +3,24 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% if company.is_manufacturer %}
|
{% if company.is_manufacturer %}
|
||||||
{% include "sidebar_item.html" with label='manufacturer-parts' text="Manufactured Parts" icon="fa-industry" %}
|
{% trans "Manufactured Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='manufacturer-parts' text=text icon="fa-industry" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if company.is_supplier %}
|
{% if company.is_supplier %}
|
||||||
{% include "sidebar_item.html" with label='supplier-parts' text="Supplied Parts" icon="fa-building" %}
|
{% trans "Supplied Parts" as text %}
|
||||||
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
|
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
|
||||||
|
{% trans "Purchase Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if company.is_manufacturer or company.is_supplier %}
|
{% if company.is_manufacturer or company.is_supplier %}
|
||||||
{% include "sidebar_item.html" with label='company-stock' text="Supplied Stock Items" icon="fa-boxes" %}
|
{% trans "Supplied Stock Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='company-stock' text=text icon="fa-boxes" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if company.is_customer %}
|
{% if company.is_customer %}
|
||||||
{% include "sidebar_item.html" with label='sales-orders' text="Sales Orders" icon="fa-truck" %}
|
{% trans "Sales Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label='assigned-stock' text="Assigned Stock Items" icon="fa-sign-out-alt" %}
|
{% include "sidebar_item.html" with label='sales-orders' text=text icon="fa-truck" %}
|
||||||
|
{% trans "Assigned Stock Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='assigned-stock' text=text icon="fa-sign-out-alt" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label='company-notes' text="Notes" icon="fa-clipboard" %}
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='company-notes' text=text icon="fa-clipboard" %}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
{% inventree_title %} | {% trans "Supplier Part" %}
|
{% inventree_title %} | {% trans "Supplier Part" %}
|
||||||
{% endblock %}
|
{% endblock page_title %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% include "company/supplier_part_sidebar.html" %}
|
{% include "company/supplier_part_sidebar.html" %}
|
||||||
{% endblock %}
|
{% endblock sidebar %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
<li class='breadcrumb-item'><a href='{% url "supplier-index" %}'>{% trans "Suppliers" %}</a></li>
|
<li class='breadcrumb-item'><a href='{% url "supplier-index" %}'>{% trans "Suppliers" %}</a></li>
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<li class='breadcrumb-item'><a href='{% url "company-detail" part.supplier.id %}'>{{ part.supplier.name }}</a></li>
|
<li class='breadcrumb-item'><a href='{% url "company-detail" part.supplier.id %}'>{{ part.supplier.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "supplier-part-detail" part.id %}'>{{ part.SKU }}</a></li>
|
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "supplier-part-detail" part.id %}'>{{ part.SKU }}</a></li>
|
||||||
{% endblock %}
|
{% endblock breadcrumbs %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
<h4>
|
<h4>
|
||||||
{% trans "Supplier Part" %}: {{ part.SKU }}
|
{% trans "Supplier Part" %}: {{ part.SKU }}
|
||||||
</h4>
|
</h4>
|
||||||
{% endblock %}
|
{% endblock heading %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
{% if user.is_staff and perms.company.change_company %}
|
{% if user.is_staff and perms.company.change_company %}
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<span class='fas fa-trash-alt icon-red'/>
|
<span class='fas fa-trash-alt icon-red'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<img class='part-thumb'
|
<img class='part-thumb'
|
||||||
@@ -56,39 +56,32 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
<p>
|
<table class='table table-striped table-condensed'>
|
||||||
{{ part.part.full_name }}
|
<col width='25'>
|
||||||
</p>
|
<tr>
|
||||||
|
<td><span class='fas fa-shapes'></span></td>
|
||||||
|
<td>{% trans "Internal Part" %}</td>
|
||||||
|
<td>
|
||||||
|
{% if part.part %}
|
||||||
|
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if part.description %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{% trans "Description" %}</td>
|
||||||
|
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock details %}
|
||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
|
|
||||||
<table class="table table-striped table-condensed">
|
<table class="table table-striped table-condensed">
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
|
||||||
<td>{% trans "Internal Part" %}</td>
|
|
||||||
<td>
|
|
||||||
{% if part.part %}
|
|
||||||
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if part.description %}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>{% trans "Description" %}</td>
|
|
||||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.link %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-link'></span></td>
|
|
||||||
<td>{% trans "External Link" %}</td>
|
|
||||||
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-building'></span></td>
|
<td><span class='fas fa-building'></span></td>
|
||||||
<td>{% trans "Supplier" %}</td>
|
<td>{% trans "Supplier" %}</td>
|
||||||
@@ -127,6 +120,13 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{{ part.note }}{% include "clip.html"%}</td>
|
<td>{{ part.note }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if part.link %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-link'></span></td>
|
||||||
|
<td>{% trans "External Link" %}</td>
|
||||||
|
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='stock' text="Stock Items" icon="fa-boxes" %}
|
{% trans "Stock Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
|
{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
|
||||||
{% include "sidebar_item.html" with label='pricing' text="Supplier Part Pricing" icon="fa-dollar-sign" %}
|
{% trans "Purchase Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
|
||||||
|
{% trans "Supplier Part Pricing" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='pricing' text=text icon="fa-dollar-sign" %}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
|||||||
data = {
|
data = {
|
||||||
'MPN': 'MPN-TEST-123',
|
'MPN': 'MPN-TEST-123',
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.patch(url, data, format='json')
|
response = self.client.patch(url, data, format='json')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ company_urls = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
manufacturer_part_urls = [
|
manufacturer_part_urls = [
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ class PartLabelMixin:
|
|||||||
if key in params:
|
if key in params:
|
||||||
parts = params.getlist(key, [])
|
parts = params.getlist(key, [])
|
||||||
break
|
break
|
||||||
|
|
||||||
valid_ids = []
|
valid_ids = []
|
||||||
|
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class LabelTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
template_string = Template(self.filename_pattern)
|
template_string = Template(self.filename_pattern)
|
||||||
|
|
||||||
ctx = self.context(request)
|
ctx = self.context(request)
|
||||||
|
|
||||||
context = Context(ctx)
|
context = Context(ctx)
|
||||||
|
|||||||
Binary file not shown.
+3285
-3079
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
+3656
-3277
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3321
-3115
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3241
-3035
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3079
-2873
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3935
-3728
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3067
-2861
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3072
-2866
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3079
-2873
File diff suppressed because it is too large
Load Diff
+1387
-1151
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3074
-2868
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3128
-2922
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3093
-2887
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3089
-2883
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ class POList(generics.ListCreateAPIView):
|
|||||||
"""
|
"""
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
item = serializer.save()
|
item = serializer.save()
|
||||||
item.created_by = request.user
|
item.created_by = request.user
|
||||||
item.save()
|
item.save()
|
||||||
@@ -404,7 +404,7 @@ class SOList(generics.ListCreateAPIView):
|
|||||||
"""
|
"""
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
item = serializer.save()
|
item = serializer.save()
|
||||||
item.created_by = request.user
|
item.created_by = request.user
|
||||||
item.save()
|
item.save()
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-11-28 01:51
|
||||||
|
|
||||||
|
import InvenTree.fields
|
||||||
|
import InvenTree.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0052_auto_20211014_0631'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchaseorderattachment',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesorderattachment',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorderattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-12-01 21:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0053_auto_20211128_0151'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='reference_int',
|
||||||
|
field=models.BigIntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='reference_int',
|
||||||
|
field=models.BigIntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -772,7 +772,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
def get_base_part(self):
|
def get_base_part(self):
|
||||||
"""
|
"""
|
||||||
Return the base part.Part object for the line item
|
Return the base part.Part object for the line item
|
||||||
|
|
||||||
Note: Returns None if the SupplierPart is not set!
|
Note: Returns None if the SupplierPart is not set!
|
||||||
"""
|
"""
|
||||||
if self.part is None:
|
if self.part is None:
|
||||||
|
|||||||
@@ -17,16 +17,16 @@ from rest_framework.serializers import ValidationError
|
|||||||
|
|
||||||
from sql_util.utils import SubqueryCount
|
from sql_util.utils import SubqueryCount
|
||||||
|
|
||||||
|
from common.settings import currency_code_mappings
|
||||||
|
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
||||||
|
|
||||||
from InvenTree.serializers import InvenTreeAttachmentSerializer
|
from InvenTree.serializers import InvenTreeAttachmentSerializer
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
from InvenTree.serializers import InvenTreeDecimalField
|
from InvenTree.serializers import InvenTreeDecimalField
|
||||||
from InvenTree.serializers import InvenTreeMoneySerializer
|
from InvenTree.serializers import InvenTreeMoneySerializer
|
||||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
from InvenTree.serializers import ReferenceIndexingSerializerMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import StockStatus
|
from InvenTree.status_codes import StockStatus
|
||||||
|
|
||||||
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
|
||||||
|
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
import stock.models
|
import stock.models
|
||||||
@@ -37,10 +37,10 @@ from .models import PurchaseOrderAttachment, SalesOrderAttachment
|
|||||||
from .models import SalesOrder, SalesOrderLineItem
|
from .models import SalesOrder, SalesOrderLineItem
|
||||||
from .models import SalesOrderAllocation
|
from .models import SalesOrderAllocation
|
||||||
|
|
||||||
from common.settings import currency_code_mappings
|
from users.serializers import OwnerSerializer
|
||||||
|
|
||||||
|
|
||||||
class POSerializer(InvenTreeModelSerializer):
|
class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||||
""" Serializer for a PurchaseOrder object """
|
""" Serializer for a PurchaseOrder object """
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -86,6 +86,8 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
reference = serializers.CharField(required=True)
|
reference = serializers.CharField(required=True)
|
||||||
|
|
||||||
|
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
'overdue',
|
'overdue',
|
||||||
'reference',
|
'reference',
|
||||||
'responsible',
|
'responsible',
|
||||||
|
'responsible_detail',
|
||||||
'supplier',
|
'supplier',
|
||||||
'supplier_detail',
|
'supplier_detail',
|
||||||
'supplier_reference',
|
'supplier_reference',
|
||||||
@@ -374,8 +377,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
Serializers for the PurchaseOrderAttachment model
|
Serializers for the PurchaseOrderAttachment model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrderAttachment
|
model = PurchaseOrderAttachment
|
||||||
|
|
||||||
@@ -383,6 +384,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'order',
|
'order',
|
||||||
'attachment',
|
'attachment',
|
||||||
|
'link',
|
||||||
'filename',
|
'filename',
|
||||||
'comment',
|
'comment',
|
||||||
'upload_date',
|
'upload_date',
|
||||||
@@ -393,7 +395,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderSerializer(InvenTreeModelSerializer):
|
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializers for the SalesOrder object
|
Serializers for the SalesOrder object
|
||||||
"""
|
"""
|
||||||
@@ -553,10 +555,10 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
||||||
|
|
||||||
quantity = InvenTreeDecimalField()
|
quantity = InvenTreeDecimalField()
|
||||||
|
|
||||||
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
||||||
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
|
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
|
||||||
|
|
||||||
sale_price = InvenTreeMoneySerializer(
|
sale_price = InvenTreeMoneySerializer(
|
||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
@@ -594,8 +596,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
Serializers for the SalesOrderAttachment model
|
Serializers for the SalesOrderAttachment model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SalesOrderAttachment
|
model = SalesOrderAttachment
|
||||||
|
|
||||||
@@ -604,6 +604,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
'order',
|
'order',
|
||||||
'attachment',
|
'attachment',
|
||||||
'filename',
|
'filename',
|
||||||
|
'link',
|
||||||
'comment',
|
'comment',
|
||||||
'upload_date',
|
'upload_date',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -53,15 +53,17 @@
|
|||||||
<span class='fas fa-shopping-cart icon-blue'></span>
|
<span class='fas fa-shopping-cart icon-blue'></span>
|
||||||
</button>
|
</button>
|
||||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||||
<button type='button' class='btn btn-outline-secondary' id='receive-order' title='{% trans "Receive items" %}'>
|
<button type='button' class='btn btn-primary' id='receive-order' title='{% trans "Receive items" %}'>
|
||||||
<span class='fas fa-sign-in-alt icon-blue'></span>
|
<span class='fas fa-sign-in-alt'></span>
|
||||||
|
{% trans "Receive Items" %}
|
||||||
</button>
|
</button>
|
||||||
<button type='button' class='btn btn-outline-secondary' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
<button type='button' class='btn btn-success' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
||||||
<span class='fas fa-check-circle icon-green'></span>
|
<span class='fas fa-check-circle'></span>
|
||||||
|
{% trans "Complete Order" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<img class='part-thumb'
|
<img class='part-thumb'
|
||||||
@@ -75,24 +77,18 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
<h4>
|
<table class='table table-striped table-condensed'>
|
||||||
{% purchase_order_status_label order.status large=True %}
|
|
||||||
{% if order.is_overdue %}
|
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</h4>
|
|
||||||
<p>{{ order.description }}{% include "clip.html"%}</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block details_right %}
|
|
||||||
<table class='table'>
|
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
<td>{% trans "Order Reference" %}</td>
|
<td>{% trans "Order Reference" %}</td>
|
||||||
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
|
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-info-circle'></span></td>
|
||||||
|
<td>{% trans "Order Description" %}</td>
|
||||||
|
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Order Status" %}</td>
|
<td>{% trans "Order Status" %}</td>
|
||||||
@@ -103,6 +99,14 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block details_right %}
|
||||||
|
<table class='table table-condensed table-striped'>
|
||||||
|
<col width='25'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-building'></span></td>
|
<td><span class='fas fa-building'></span></td>
|
||||||
<td>{% trans "Supplier" %}</td>
|
<td>{% trans "Supplier" %}</td>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% url "po-detail" order.id as url %}
|
{% url "po-detail" order.id as url %}
|
||||||
{% include "sidebar_item.html" with url=url text="Return to Orders" icon="fa-undo" %}
|
{% trans "Return to Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with url=url text=text icon="fa-undo" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
|
{% trans "Line Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='received-items' text="Received Stock" icon="fa-sign-in-alt" %}
|
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
|
||||||
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
|
{% trans "Received Stock" as text %}
|
||||||
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
|
{% include "sidebar_item.html" with label='received-items' text=text icon="fa-sign-in-alt" %}
|
||||||
|
{% trans "Attachments" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
|
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
|
||||||
</button>
|
</button>
|
||||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||||
<button type='button' class='btn btn-success' id='receive-selected-items' title='{% trans "Receive selected items" %}'>
|
<button type='button' class='btn btn-primary' id='receive-selected-items' title='{% trans "Receive selected items" %}'>
|
||||||
<span class='fas fa-sign-in-alt'></span> {% trans "Receive Items" %}
|
<span class='fas fa-sign-in-alt'></span> {% trans "Receive Items" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -124,51 +124,16 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
loadAttachmentTable(
|
loadAttachmentTable('{% url "api-po-attachment-list" %}', {
|
||||||
'{% url "api-po-attachment-list" %}',
|
filters: {
|
||||||
{
|
order: {{ order.pk }},
|
||||||
filters: {
|
},
|
||||||
order: {{ order.pk }},
|
fields: {
|
||||||
},
|
order: {
|
||||||
onEdit: function(pk) {
|
value: {{ order.pk }},
|
||||||
var url = `/api/order/po/attachment/${pk}/`;
|
hidden: true,
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
|
||||||
filename: {},
|
|
||||||
comment: {},
|
|
||||||
},
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
title: '{% trans "Edit Attachment" %}',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDelete: function(pk) {
|
|
||||||
|
|
||||||
constructForm(`/api/order/po/attachment/${pk}/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
|
||||||
title: '{% trans "Delete Attachment" %}',
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
$("#new-attachment").click(function() {
|
|
||||||
|
|
||||||
constructForm('{% url "api-po-attachment-list" %}', {
|
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
attachment: {},
|
|
||||||
comment: {},
|
|
||||||
order: {
|
|
||||||
value: {{ order.pk }},
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reload: true,
|
|
||||||
title: '{% trans "Add Attachment" %}',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadStockTable($("#stock-table"), {
|
loadStockTable($("#stock-table"), {
|
||||||
|
|||||||
@@ -68,17 +68,33 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
<h4>
|
<table class='table table-striped table-condensed'>
|
||||||
{% sales_order_status_label order.status large=True %}
|
<col width='25'>
|
||||||
{% if order.is_overdue %}
|
<tr>
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
{% endif %}
|
<td>{% trans "Order Reference" %}</td>
|
||||||
</h4>
|
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
|
||||||
<p>{{ order.description }}{% include "clip.html"%}</p>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-info-circle'></span></td>
|
||||||
|
<td>{% trans "Order Description" %}</td>
|
||||||
|
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-info'></span></td>
|
||||||
|
<td>{% trans "Order Status" %}</td>
|
||||||
|
<td>
|
||||||
|
{% sales_order_status_label order.status %}
|
||||||
|
{% if order.is_overdue %}
|
||||||
|
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<div class='info-messages'>
|
<div class='info-messages'>
|
||||||
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
|
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
|
||||||
@@ -93,21 +109,6 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-info'></span></td>
|
|
||||||
<td>{% trans "Order Status" %}</td>
|
|
||||||
<td>
|
|
||||||
{% sales_order_status_label order.status %}
|
|
||||||
{% if order.is_overdue %}
|
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if order.customer %}
|
{% if order.customer %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-building'></span></td>
|
<td><span class='fas fa-building'></span></td>
|
||||||
|
|||||||
@@ -110,55 +110,21 @@
|
|||||||
},
|
},
|
||||||
label: 'attachment',
|
label: 'attachment',
|
||||||
success: function(data, status, xhr) {
|
success: function(data, status, xhr) {
|
||||||
location.reload();
|
reloadAttachmentTable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
loadAttachmentTable(
|
loadAttachmentTable('{% url "api-so-attachment-list" %}', {
|
||||||
'{% url "api-so-attachment-list" %}',
|
filters: {
|
||||||
{
|
order: {{ order.pk }},
|
||||||
filters: {
|
},
|
||||||
order: {{ order.pk }},
|
fields: {
|
||||||
|
order: {
|
||||||
|
value: {{ order.pk }},
|
||||||
|
hidden: true,
|
||||||
},
|
},
|
||||||
onEdit: function(pk) {
|
|
||||||
var url = `/api/order/so/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
|
||||||
filename: {},
|
|
||||||
comment: {},
|
|
||||||
},
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
title: '{% trans "Edit Attachment" %}',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDelete: function(pk) {
|
|
||||||
constructForm(`/api/order/so/attachment/${pk}/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
|
||||||
title: '{% trans "Delete Attachment" %}',
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
$("#new-attachment").click(function() {
|
|
||||||
|
|
||||||
constructForm('{% url "api-so-attachment-list" %}', {
|
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
attachment: {},
|
|
||||||
comment: {},
|
|
||||||
order: {
|
|
||||||
value: {{ order.pk }},
|
|
||||||
hidden: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
title: '{% trans "Add Attachment" %}'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBuildTable($("#builds-table"), {
|
loadBuildTable($("#builds-table"), {
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
|
{% trans "Line Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='order-builds' text="Build Orders" icon="fa-tools" %}
|
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
|
||||||
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
|
{% trans "Build Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
|
{% include "sidebar_item.html" with label='order-builds' text=text icon="fa-tools" %}
|
||||||
|
{% trans "Attachments" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}
|
||||||
|
|||||||
@@ -105,6 +105,25 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
self.assertEqual(data['pk'], 1)
|
self.assertEqual(data['pk'], 1)
|
||||||
self.assertEqual(data['description'], 'Ordering some screws')
|
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"""
|
||||||
|
# get permissions
|
||||||
|
self.assignRole('purchase_order.add')
|
||||||
|
|
||||||
|
url = reverse('api-po-list')
|
||||||
|
huge_numer = 9223372036854775808
|
||||||
|
|
||||||
|
# too big
|
||||||
|
self.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'supplier': 1,
|
||||||
|
'reference': huge_numer,
|
||||||
|
'description': 'PO not created via the API',
|
||||||
|
},
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
def test_po_attachments(self):
|
def test_po_attachments(self):
|
||||||
|
|
||||||
url = reverse('api-po-attachment-list')
|
url = reverse('api-po-attachment-list')
|
||||||
@@ -228,7 +247,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.assignRole('purchase_order.add')
|
self.assignRole('purchase_order.add')
|
||||||
|
|
||||||
self.url = reverse('api-po-receive', kwargs={'pk': 1})
|
self.url = reverse('api-po-receive', kwargs={'pk': 1})
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
|||||||
|
|
||||||
def done(self, form_list, **kwargs):
|
def done(self, form_list, **kwargs):
|
||||||
""" Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """
|
""" Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """
|
||||||
|
|
||||||
order = self.get_order()
|
order = self.get_order()
|
||||||
items = self.get_clean_items()
|
items = self.get_clean_items()
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
|||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# PurchaseOrderLineItem already exists
|
# PurchaseOrderLineItem already exists
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
|
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
|
||||||
|
|
||||||
|
|
||||||
@@ -449,7 +449,7 @@ class SalesOrderExport(AjaxView):
|
|||||||
role_required = 'sales_order.view'
|
role_required = 'sales_order.view'
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
|
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
|
||||||
|
|
||||||
export_format = request.GET.get('format', 'csv')
|
export_format = request.GET.get('format', 'csv')
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ class BomItemResource(ModelResource):
|
|||||||
|
|
||||||
# If we are not generating an "import" template,
|
# If we are not generating an "import" template,
|
||||||
# just return the complete list of fields
|
# just return the complete list of fields
|
||||||
if not self.is_importing:
|
if not getattr(self, 'is_importing', False):
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
# Otherwise, remove some fields we are not interested in
|
# Otherwise, remove some fields we are not interested in
|
||||||
|
|||||||
+147
-28
@@ -26,7 +26,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
|||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
from .models import Part, PartCategory
|
from .models import Part, PartCategory, PartRelated
|
||||||
from .models import BomItem, BomItemSubstitute
|
from .models import BomItem, BomItemSubstitute
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
from .models import PartAttachment, PartTestTemplate
|
from .models import PartAttachment, PartTestTemplate
|
||||||
@@ -42,7 +42,7 @@ from build.models import Build
|
|||||||
|
|
||||||
from . import serializers as part_serializers
|
from . import serializers as part_serializers
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool, isNull
|
from InvenTree.helpers import str2bool, isNull, increment
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
@@ -169,7 +169,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
"""
|
"""
|
||||||
API endpoint for detail view of a single PartCategory object
|
API endpoint for detail view of a single PartCategory object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = part_serializers.CategorySerializer
|
serializer_class = part_serializers.CategorySerializer
|
||||||
queryset = PartCategory.objects.all()
|
queryset = PartCategory.objects.all()
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ class CategoryParameterList(generics.ListAPIView):
|
|||||||
|
|
||||||
if category is not None:
|
if category is not None:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
category = PartCategory.objects.get(pk=category)
|
category = PartCategory.objects.get(pk=category)
|
||||||
|
|
||||||
fetch_parent = str2bool(params.get('fetch_parent', True))
|
fetch_parent = str2bool(params.get('fetch_parent', True))
|
||||||
@@ -410,6 +410,33 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartSerialNumberDetail(generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for returning extra serial number information about a particular part
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = Part.objects.all()
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
part = self.get_object()
|
||||||
|
|
||||||
|
# Calculate the "latest" serial number
|
||||||
|
latest = part.getLatestSerialNumber()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'latest': latest,
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest is not None:
|
||||||
|
next = increment(latest)
|
||||||
|
|
||||||
|
if next != increment:
|
||||||
|
data['next'] = next
|
||||||
|
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
""" API endpoint for detail view of a single Part object """
|
""" API endpoint for detail view of a single Part object """
|
||||||
|
|
||||||
@@ -734,7 +761,7 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'initial_stock_quantity': [_('Must be a valid quantity')],
|
'initial_stock_quantity': [_('Must be a valid quantity')],
|
||||||
})
|
})
|
||||||
|
|
||||||
initial_stock_location = request.data.get('initial_stock_location', None)
|
initial_stock_location = request.data.get('initial_stock_location', None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -850,7 +877,7 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
id_values.append(val)
|
id_values.append(val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
queryset = queryset.exclude(pk__in=id_values)
|
queryset = queryset.exclude(pk__in=id_values)
|
||||||
|
|
||||||
# Exclude part variant tree?
|
# Exclude part variant tree?
|
||||||
@@ -901,6 +928,40 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = queryset.filter(pk__in=pks)
|
queryset = queryset.filter(pk__in=pks)
|
||||||
|
|
||||||
|
# Filter by 'related' parts?
|
||||||
|
related = params.get('related', None)
|
||||||
|
exclude_related = params.get('exclude_related', None)
|
||||||
|
|
||||||
|
if related is not None or exclude_related is not None:
|
||||||
|
try:
|
||||||
|
pk = related if related is not None else exclude_related
|
||||||
|
pk = int(pk)
|
||||||
|
|
||||||
|
related_part = Part.objects.get(pk=pk)
|
||||||
|
|
||||||
|
part_ids = set()
|
||||||
|
|
||||||
|
# Return any relationship which points to the part in question
|
||||||
|
relation_filter = Q(part_1=related_part) | Q(part_2=related_part)
|
||||||
|
|
||||||
|
for relation in PartRelated.objects.filter(relation_filter):
|
||||||
|
|
||||||
|
if relation.part_1.pk != pk:
|
||||||
|
part_ids.add(relation.part_1.pk)
|
||||||
|
|
||||||
|
if relation.part_2.pk != pk:
|
||||||
|
part_ids.add(relation.part_2.pk)
|
||||||
|
|
||||||
|
if related is not None:
|
||||||
|
# Only return related results
|
||||||
|
queryset = queryset.filter(pk__in=[pk for pk in part_ids])
|
||||||
|
elif exclude_related is not None:
|
||||||
|
# Exclude related results
|
||||||
|
queryset = queryset.exclude(pk__in=[pk for pk in part_ids])
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
# Filter by 'starred' parts?
|
# Filter by 'starred' parts?
|
||||||
starred = params.get('starred', None)
|
starred = params.get('starred', None)
|
||||||
|
|
||||||
@@ -1014,9 +1075,48 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
'revision',
|
'revision',
|
||||||
'keywords',
|
'keywords',
|
||||||
'category__name',
|
'category__name',
|
||||||
|
'manufacturer_parts__MPN',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartRelatedList(generics.ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for accessing a list of PartRelated objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PartRelated.objects.all()
|
||||||
|
serializer_class = part_serializers.PartRelationSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
# Add a filter for "part" - we can filter either part_1 or part_2
|
||||||
|
part = params.get('part', None)
|
||||||
|
|
||||||
|
if part is not None:
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(pk=part)
|
||||||
|
|
||||||
|
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part))
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for accessing detail view of a PartRelated object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PartRelated.objects.all()
|
||||||
|
serializer_class = part_serializers.PartRelationSerializer
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplateList(generics.ListCreateAPIView):
|
class PartParameterTemplateList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PartParameterTemplate objects.
|
""" API endpoint for accessing a list of PartParameterTemplate objects.
|
||||||
|
|
||||||
@@ -1081,24 +1181,6 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
|
inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
|
||||||
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
|
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
|
||||||
|
|
||||||
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
|
|
||||||
|
|
||||||
def filter_validated(self, queryset, name, value):
|
|
||||||
|
|
||||||
# Work out which lines have actually been validated
|
|
||||||
pks = []
|
|
||||||
|
|
||||||
for bom_item in queryset.all():
|
|
||||||
if bom_item.is_line_valid():
|
|
||||||
pks.append(bom_item.pk)
|
|
||||||
|
|
||||||
if str2bool(value):
|
|
||||||
queryset = queryset.filter(pk__in=pks)
|
|
||||||
else:
|
|
||||||
queryset = queryset.exclude(pk__in=pks)
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
# Filters for linked 'part'
|
# Filters for linked 'part'
|
||||||
part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active')
|
part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active')
|
||||||
part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable')
|
part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable')
|
||||||
@@ -1107,6 +1189,30 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable')
|
sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable')
|
||||||
sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly')
|
sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly')
|
||||||
|
|
||||||
|
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
|
||||||
|
|
||||||
|
def filter_validated(self, queryset, name, value):
|
||||||
|
|
||||||
|
# Work out which lines have actually been validated
|
||||||
|
pks = []
|
||||||
|
|
||||||
|
value = str2bool(value)
|
||||||
|
|
||||||
|
# Shortcut for quicker filtering - BomItem with empty 'checksum' values are not validated
|
||||||
|
if value:
|
||||||
|
queryset = queryset.exclude(checksum=None).exclude(checksum='')
|
||||||
|
|
||||||
|
for bom_item in queryset.all():
|
||||||
|
if bom_item.is_line_valid:
|
||||||
|
pks.append(bom_item.pk)
|
||||||
|
|
||||||
|
if value:
|
||||||
|
queryset = queryset.filter(pk__in=pks)
|
||||||
|
else:
|
||||||
|
queryset = queryset.exclude(pk__in=pks)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class BomList(generics.ListCreateAPIView):
|
class BomList(generics.ListCreateAPIView):
|
||||||
"""
|
"""
|
||||||
@@ -1257,7 +1363,7 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
queryset = self.annotate_pricing(queryset)
|
queryset = self.annotate_pricing(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def include_pricing(self):
|
def include_pricing(self):
|
||||||
"""
|
"""
|
||||||
Determine if pricing information should be included in the response
|
Determine if pricing information should be included in the response
|
||||||
@@ -1291,7 +1397,7 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
# Get default currency from settings
|
# Get default currency from settings
|
||||||
default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
if currency and default_currency:
|
if currency and default_currency:
|
||||||
try:
|
try:
|
||||||
@@ -1381,7 +1487,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
serializer_class = part_serializers.BomItemSubstituteSerializer
|
serializer_class = part_serializers.BomItemSubstituteSerializer
|
||||||
queryset = BomItemSubstitute.objects.all()
|
queryset = BomItemSubstitute.objects.all()
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
DjangoFilterBackend,
|
DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
@@ -1435,6 +1541,12 @@ part_api_urls = [
|
|||||||
url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
|
url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
# Base URL for PartRelated API endpoints
|
||||||
|
url(r'^related/', include([
|
||||||
|
url(r'^(?P<pk>\d+)/', PartRelatedDetail.as_view(), name='api-part-related-detail'),
|
||||||
|
url(r'^.*$', PartRelatedList.as_view(), name='api-part-related-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
# Base URL for PartParameter API endpoints
|
# Base URL for PartParameter API endpoints
|
||||||
url(r'^parameter/', include([
|
url(r'^parameter/', include([
|
||||||
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
|
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
|
||||||
@@ -1448,7 +1560,14 @@ part_api_urls = [
|
|||||||
url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
|
url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'),
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
|
|
||||||
|
# Endpoint for extra serial number information
|
||||||
|
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
|
||||||
|
|
||||||
|
# Part detail endpoint
|
||||||
|
url(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
|
||||||
|
])),
|
||||||
|
|
||||||
url(r'^.*$', PartList.as_view(), name='api-part-list'),
|
url(r'^.*$', PartList.as_view(), name='api-part-list'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
uids = []
|
uids = []
|
||||||
|
|
||||||
def add_items(items, level, cascade):
|
def add_items(items, level, cascade=True):
|
||||||
# Add items at a given layer
|
# Add items at a given layer
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
# Filter manufacturer parts
|
# Filter manufacturer parts
|
||||||
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk).prefetch_related('supplier_parts')
|
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk).prefetch_related('supplier_parts')
|
||||||
|
|
||||||
for mp_idx, mp_part in enumerate(manufacturer_parts):
|
for mp_idx, mp_part in enumerate(manufacturer_parts):
|
||||||
|
|
||||||
# Extract the "name" field of the Manufacturer (Company)
|
# Extract the "name" field of the Manufacturer (Company)
|
||||||
@@ -190,7 +190,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
# Generate a column name for this manufacturer
|
# Generate a column name for this manufacturer
|
||||||
k_man = f'{_("Manufacturer")}_{mp_idx}'
|
k_man = f'{_("Manufacturer")}_{mp_idx}'
|
||||||
k_mpn = f'{_("MPN")}_{mp_idx}'
|
k_mpn = f'{_("MPN")}_{mp_idx}'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manufacturer_cols[k_man].update({bom_idx: manufacturer_name})
|
manufacturer_cols[k_man].update({bom_idx: manufacturer_name})
|
||||||
manufacturer_cols[k_mpn].update({bom_idx: manufacturer_mpn})
|
manufacturer_cols[k_mpn].update({bom_idx: manufacturer_mpn})
|
||||||
@@ -200,7 +200,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
# We wish to include supplier data for this manufacturer part
|
# We wish to include supplier data for this manufacturer part
|
||||||
if supplier_data:
|
if supplier_data:
|
||||||
|
|
||||||
for sp_idx, sp_part in enumerate(mp_part.supplier_parts.all()):
|
for sp_idx, sp_part in enumerate(mp_part.supplier_parts.all()):
|
||||||
|
|
||||||
supplier_parts_used.add(sp_part)
|
supplier_parts_used.add(sp_part)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user