mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
merge
This commit is contained in:
commit
0f0460f8ea
37
.github/workflows/check_translations.yaml
vendored
Normal file
37
.github/workflows/check_translations.yaml
vendored
Normal file
@ -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
|
9
.github/workflows/html.yaml
vendored
9
.github/workflows/html.yaml
vendored
@ -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
|
||||||
|
9
.github/workflows/javascript.yaml
vendored
9
.github/workflows/javascript.yaml
vendored
@ -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
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -78,5 +78,4 @@ locale_stats.json
|
|||||||
|
|
||||||
# node.js
|
# node.js
|
||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
|
||||||
node_modules/
|
node_modules/
|
@ -2,8 +2,7 @@
|
|||||||
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
|
||||||
|
@ -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,6 +72,12 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
|
|
||||||
reference = getattr(self, 'reference', '')
|
reference = getattr(self, 'reference', '')
|
||||||
|
|
||||||
|
self.reference_int = extract_int(reference)
|
||||||
|
|
||||||
|
reference_int = models.BigIntegerField(default=0)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_int(reference):
|
||||||
# Default value if we cannot convert to an integer
|
# Default value if we cannot convert to an integer
|
||||||
ref_int = 0
|
ref_int = 0
|
||||||
|
|
||||||
@ -80,21 +90,21 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
ref_int = int(ref)
|
ref_int = int(ref)
|
||||||
except:
|
except:
|
||||||
ref_int = 0
|
ref_int = 0
|
||||||
|
return ref_int
|
||||||
self.reference_int = ref_int
|
|
||||||
|
|
||||||
reference_int = models.IntegerField(default=0)
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
if self.attachment is not None:
|
||||||
return os.path.basename(self.attachment.name)
|
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):
|
||||||
|
if self.attachment:
|
||||||
return os.path.basename(self.attachment.name)
|
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):
|
||||||
"""
|
"""
|
||||||
@ -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 {
|
||||||
|
@ -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
|
||||||
@ -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
|
||||||
|
@ -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
|
||||||
|
25
InvenTree/build/migrations/0033_auto_20211128_0151.py
Normal file
25
InvenTree/build/migrations/0033_auto_20211128_0151.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/build/migrations/0034_alter_build_reference_int.py
Normal file
18
InvenTree/build/migrations/0034_alter_build_reference_int.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
"""
|
"""
|
||||||
@ -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: {},
|
|
||||||
comment: {},
|
|
||||||
build: {
|
build: {
|
||||||
value: {{ build.pk }},
|
value: {{ build.pk }},
|
||||||
hidden: true,
|
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" %}
|
||||||
|
@ -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:
|
||||||
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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>
|
||||||
|
<ul class='dropdown-menu' role='menu'>
|
||||||
|
{% if perms.company.change_company %}
|
||||||
|
<li><a class='dropdown-item' href='#' id='company-edit' title='{% trans "Edit company information" %}'>
|
||||||
|
<span class='fas fa-edit icon-green'></span> {% trans "Edit Company" %}
|
||||||
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.company.delete_company %}
|
{% if perms.company.delete_company %}
|
||||||
<button type='button' class='btn btn-outline-secondary' id='company-delete' title='{% trans "Delete Company" %}'>
|
<li><a class='dropdown-item' href='#' id='company-delete' title='{% trans "Delete company" %}'>
|
||||||
<span class='fas fa-trash-alt icon-red'/>
|
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Company" %}
|
||||||
</button>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
</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,15 +55,11 @@ 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'>
|
||||||
|
|
||||||
{% block details_right %}
|
|
||||||
|
|
||||||
<table class="table table-striped table-condensed">
|
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
<td><span class='fas fa-shapes'></span></td>
|
||||||
@ -81,6 +77,25 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock details %}
|
||||||
|
|
||||||
|
{% block details_right %}
|
||||||
|
|
||||||
|
<table class="table table-striped table-condensed">
|
||||||
|
<col width='25'>
|
||||||
|
|
||||||
|
<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>
|
||||||
{% if part.link %}
|
{% if part.link %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-link'></span></td>
|
<td><span class='fas fa-link'></span></td>
|
||||||
@ -88,17 +103,8 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% 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,15 +56,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
<p>
|
<table class='table table-striped table-condensed'>
|
||||||
{{ part.part.full_name }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block details_right %}
|
|
||||||
|
|
||||||
<table class="table table-striped table-condensed">
|
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
<td><span class='fas fa-shapes'></span></td>
|
||||||
@ -82,13 +74,14 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.link %}
|
</table>
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-link'></span></td>
|
{% endblock details %}
|
||||||
<td>{% trans "External Link" %}</td>
|
|
||||||
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
{% block details_right %}
|
||||||
</tr>
|
|
||||||
{% endif %}
|
<table class="table table-striped table-condensed">
|
||||||
|
<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>
|
||||||
@ -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" %}
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/el/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/el/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/he/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/he/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/id/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/id/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/ko/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/ko/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/nl/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/nl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/no/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/no/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/sv/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/sv/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/th/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/th/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/vi/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/vi/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
35
InvenTree/order/migrations/0053_auto_20211128_0151.py
Normal file
35
InvenTree/order/migrations/0053_auto_20211128_0151.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
23
InvenTree/order/migrations/0054_auto_20211201_2139.py
Normal file
23
InvenTree/order/migrations/0054_auto_20211201_2139.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
"""
|
"""
|
||||||
@ -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: {
|
filters: {
|
||||||
order: {{ order.pk }},
|
order: {{ order.pk }},
|
||||||
},
|
},
|
||||||
onEdit: function(pk) {
|
|
||||||
var url = `/api/order/po/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
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: {
|
order: {
|
||||||
value: {{ order.pk }},
|
value: {{ order.pk }},
|
||||||
hidden: true,
|
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'>
|
||||||
|
<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-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 %}
|
{% if order.is_overdue %}
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h4>
|
</td>
|
||||||
<p>{{ order.description }}{% include "clip.html"%}</p>
|
</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: {
|
filters: {
|
||||||
order: {{ order.pk }},
|
order: {{ order.pk }},
|
||||||
},
|
},
|
||||||
onEdit: function(pk) {
|
|
||||||
var url = `/api/order/so/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
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: {
|
order: {
|
||||||
value: {{ order.pk }},
|
value: {{ order.pk }},
|
||||||
hidden: true
|
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')
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
@ -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 """
|
||||||
|
|
||||||
@ -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):
|
||||||
"""
|
"""
|
||||||
@ -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:
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from InvenTree.fields import RoundingDecimalFormField
|
|||||||
import common.models
|
import common.models
|
||||||
from common.forms import MatchItemForm
|
from common.forms import MatchItemForm
|
||||||
|
|
||||||
from .models import Part, PartCategory, PartRelated
|
from .models import Part, PartCategory
|
||||||
from .models import PartParameterTemplate
|
from .models import PartParameterTemplate
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
@ -157,20 +157,6 @@ class BomMatchItemForm(MatchItemForm):
|
|||||||
return super().get_special_field(col_guess, row, file_manager)
|
return super().get_special_field(col_guess, row, file_manager)
|
||||||
|
|
||||||
|
|
||||||
class CreatePartRelatedForm(HelperForm):
|
|
||||||
""" Form for creating a PartRelated object """
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PartRelated
|
|
||||||
fields = [
|
|
||||||
'part_1',
|
|
||||||
'part_2',
|
|
||||||
]
|
|
||||||
labels = {
|
|
||||||
'part_2': _('Related Part'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SetPartCategoryForm(forms.Form):
|
class SetPartCategoryForm(forms.Form):
|
||||||
""" Form for setting the category of multiple Part objects """
|
""" Form for setting the category of multiple Part objects """
|
||||||
|
|
||||||
|
25
InvenTree/part/migrations/0075_auto_20211128_0151.py
Normal file
25
InvenTree/part/migrations/0075_auto_20211128_0151.py
Normal file
@ -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 = [
|
||||||
|
('part', '0074_partcategorystar'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partattachment',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
]
|
@ -1587,7 +1587,7 @@ class Part(MPTTModel):
|
|||||||
# Exclude any parts that this part is used *in* (to prevent recursive BOMs)
|
# Exclude any parts that this part is used *in* (to prevent recursive BOMs)
|
||||||
used_in = self.get_used_in().all()
|
used_in = self.get_used_in().all()
|
||||||
|
|
||||||
parts = parts.exclude(id__in=[item.part.id for item in used_in])
|
parts = parts.exclude(id__in=[part.id for part in used_in])
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
|||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from .models import (BomItem, BomItemSubstitute,
|
from .models import (BomItem, BomItemSubstitute,
|
||||||
Part, PartAttachment, PartCategory,
|
Part, PartAttachment, PartCategory, PartRelated,
|
||||||
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
||||||
PartStar, PartTestTemplate, PartCategoryParameterTemplate,
|
PartStar, PartTestTemplate, PartCategoryParameterTemplate,
|
||||||
PartInternalPriceBreak)
|
PartInternalPriceBreak)
|
||||||
@ -75,8 +75,6 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
Serializer for the PartAttachment class
|
Serializer for the PartAttachment class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PartAttachment
|
model = PartAttachment
|
||||||
|
|
||||||
@ -85,6 +83,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
'part',
|
'part',
|
||||||
'attachment',
|
'attachment',
|
||||||
'filename',
|
'filename',
|
||||||
|
'link',
|
||||||
'comment',
|
'comment',
|
||||||
'upload_date',
|
'upload_date',
|
||||||
]
|
]
|
||||||
@ -388,6 +387,25 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartRelationSerializer(InvenTreeModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for a PartRelated model
|
||||||
|
"""
|
||||||
|
|
||||||
|
part_1_detail = PartSerializer(source='part_1', read_only=True, many=False)
|
||||||
|
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartRelated
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'part_1',
|
||||||
|
'part_1_detail',
|
||||||
|
'part_2',
|
||||||
|
'part_2_detail',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PartStarSerializer(InvenTreeModelSerializer):
|
class PartStarSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializer for a PartStar object """
|
""" Serializer for a PartStar object """
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% url "part-detail" part.id as url %}
|
{% url "part-detail" part.id as url %}
|
||||||
{% include "sidebar_link.html" with url=url text="Return to BOM" icon="fa-undo" %}
|
{% trans "Return to BOM" as text %}
|
||||||
|
{% include "sidebar_link.html" with url=url text=text icon="fa-undo" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
|
@ -61,29 +61,43 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details_left %}
|
{% block details_left %}
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<col width='25'>
|
||||||
{% if category %}
|
{% if category %}
|
||||||
<p>{{ category.description }}</p>
|
{% if category.description %}
|
||||||
{% else %}
|
<tr>
|
||||||
<p>{% trans "Top level part category" %}</p>
|
<td><span class='fas fa-info-circle'></span></td>
|
||||||
|
<td>{% trans "Description" %}</td>
|
||||||
|
<td>{{ category.description }}</td>
|
||||||
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<tr>
|
||||||
{% endblock %}
|
<td><span class='fas fa-sitemap'></span></td>
|
||||||
|
<td>{% trans "Category Path" %}</td>
|
||||||
|
<td>{{ category.pathstring }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if category.default_keywords %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-key'></span></td>
|
||||||
|
<td>{% trans "Keywords" %}</td>
|
||||||
|
<td>{{ category.default_keywords }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-sitemap'></span></td>
|
||||||
|
<td>{% trans "Category Path" %}</td>
|
||||||
|
<td><em>{% trans "Top level part category" %}</em></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
{% endblock details_left %}
|
||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
|
|
||||||
{% if category %}
|
{% if category %}
|
||||||
<table class='table table-condensed table-striped'>
|
<table class='table table-condensed table-striped'>
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-sitemap'></span></td>
|
|
||||||
<td>{% trans "Category Path" %}</td>
|
|
||||||
<td>{{ category.pathstring }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-info-circle'></span></td>
|
|
||||||
<td>{% trans "Category Description" %}</td>
|
|
||||||
<td>{{ category.description }}</td>
|
|
||||||
</tr>
|
|
||||||
{% if category.default_location %}
|
{% if category.default_location %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||||
@ -91,13 +105,6 @@
|
|||||||
<td><a href="{% url 'stock-location-detail' category.default_location.pk %}">{{ category.default_location.pathstring }}</a></td>
|
<td><a href="{% url 'stock-location-detail' category.default_location.pk %}">{{ category.default_location.pathstring }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if category.default_keywords %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-key'></span></td>
|
|
||||||
<td>{% trans "Keywords" %}</td>
|
|
||||||
<td>{{ category.default_keywords }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-sitemap'></span></td>
|
<td><span class='fas fa-sitemap'></span></td>
|
||||||
<td>{% trans "Subcategories" %}</td>
|
<td>{% trans "Subcategories" %}</td>
|
||||||
@ -124,7 +131,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock details_right %}
|
||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
|
|
||||||
|
@ -4,12 +4,16 @@
|
|||||||
|
|
||||||
{% settings_value 'PART_SHOW_IMPORT' as show_import %}
|
{% settings_value 'PART_SHOW_IMPORT' as show_import %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label="subcategories" text="Subcategories" icon="fa-sitemap" %}
|
{% trans "Subcategories" as text %}
|
||||||
{% include "sidebar_item.html" with label="parts" text="Parts" icon="fa-shapes" %}
|
{% include "sidebar_item.html" with label="subcategories" text=text icon="fa-sitemap" %}
|
||||||
|
{% trans "Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="parts" text=text icon="fa-shapes" %}
|
||||||
{% if show_import and user.is_staff and roles.part.add %}
|
{% if show_import and user.is_staff and roles.part.add %}
|
||||||
{% url "part-import" as url %}
|
{% url "part-import" as url %}
|
||||||
{% include "sidebar_link.html" with url=url text="Import Parts" icon="fa-file-upload" %}
|
{% trans "Import Parts" as text %}
|
||||||
|
{% include "sidebar_link.html" with url=url text=text icon="fa-file-upload" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if category %}
|
{% if category %}
|
||||||
{% include "sidebar_item.html" with label="parameters" text="Parameters" icon="fa-tasks" %}
|
{% trans "Parameters" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="parameters" text=text icon="fa-tasks" %}
|
||||||
{% endif %}
|
{% endif %}
|
@ -11,113 +11,6 @@
|
|||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
|
|
||||||
<div class='panel panel-hidden' id='panel-part-details'>
|
|
||||||
<div class='panel-heading'>
|
|
||||||
<h4>{% trans "Part Details" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class='panel-content'>
|
|
||||||
|
|
||||||
<!-- Details Table -->
|
|
||||||
<table class="table table-striped table-condensed">
|
|
||||||
<col width='25'>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
|
||||||
<td>{% trans "Name" %}</td>
|
|
||||||
<td>{{ part.name }}{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-info-circle'></span></td>
|
|
||||||
<td>{% trans "Description" %}</td>
|
|
||||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
{% if part.category %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-sitemap'></span></td>
|
|
||||||
<td>{% trans "Category" %}</td>
|
|
||||||
<td>
|
|
||||||
<a href='{% url "category-detail" part.category.pk %}'>{{ part.category.name }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.IPN %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-tag'></span></td>
|
|
||||||
<td>{% trans "IPN" %}</td>
|
|
||||||
<td>{{ part.IPN }}{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.revision %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-code-branch'></span></td>
|
|
||||||
<td>{% trans "Revision" %}</td>
|
|
||||||
<td>{{ part.revision }}{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.units %}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>{% trans "Units" %}</td>
|
|
||||||
<td>{{ part.units }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.minimum_stock %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-flag'></span></td>
|
|
||||||
<td>{% trans "Minimum stock level" %}</td>
|
|
||||||
<td>{{ part.minimum_stock }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.keywords %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-key'></span></td>
|
|
||||||
<td>{% trans "Keywords" %}</td>
|
|
||||||
<td>{{ part.keywords }}{% 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>
|
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
|
||||||
<td>{% trans "Creation Date" %}</td>
|
|
||||||
<td>
|
|
||||||
{{ part.creation_date }}
|
|
||||||
{% if part.creation_user %}
|
|
||||||
<span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if part.trackable and part.getLatestSerialNumber %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
|
||||||
<td>{% trans "Latest Serial Number" %}</td>
|
|
||||||
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.default_location %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-search-location'></span></td>
|
|
||||||
<td>{% trans "Default Location" %}</td>
|
|
||||||
<td>
|
|
||||||
<a href='{% url "stock-location-detail" part.default_location.pk %}'>{{ part.default_location }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if part.default_supplier %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-building'></span></td>
|
|
||||||
<td>{% trans "Default Supplier" %}</td>
|
|
||||||
<td>{{ part.default_supplier }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='panel panel-hidden' id='panel-part-stock'>
|
<div class='panel panel-hidden' id='panel-part-stock'>
|
||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
<div class='d-flex flex-wrap'>
|
<div class='d-flex flex-wrap'>
|
||||||
@ -330,33 +223,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id='table-related-part' class='table table-condensed table-striped' data-toolbar='#related-button-toolbar'>
|
<table id='related-parts-table' class='table table-striped table-condensed' data-toolbar='#related-button-toolbar'></table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-field='part' data-serachable='true'>{% trans "Part" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item in part.get_related_parts %}
|
|
||||||
{% with part_related=item.0 part=item.1 %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a class='hover-icon'>
|
|
||||||
<img class='hover-img-thumb' src='{{ part.get_thumbnail_url }}'>
|
|
||||||
<img class='hover-img-large' src='{{ part.get_thumbnail_url }}'>
|
|
||||||
</a>
|
|
||||||
<a href='/part/{{ part.id }}/'>{{ part }}</a>
|
|
||||||
<div class='btn-group' style='float: right;'>
|
|
||||||
{% if roles.part.change %}
|
|
||||||
<button title='{% trans "Delete" %}' class='btn btn-outline-secondary delete-related-part' url="{% url 'part-related-delete' part_related.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -771,15 +638,34 @@
|
|||||||
|
|
||||||
// Load the "related parts" tab
|
// Load the "related parts" tab
|
||||||
onPanelLoad("related-parts", function() {
|
onPanelLoad("related-parts", function() {
|
||||||
$('#table-related-part').inventreeTable({
|
|
||||||
});
|
loadRelatedPartsTable(
|
||||||
|
"#related-parts-table",
|
||||||
|
{{ part.pk }}
|
||||||
|
);
|
||||||
|
|
||||||
$("#add-related-part").click(function() {
|
$("#add-related-part").click(function() {
|
||||||
launchModalForm("{% url 'part-related-create' %}", {
|
|
||||||
data: {
|
constructForm('{% url "api-part-related-list" %}', {
|
||||||
part: {{ part.id }},
|
method: 'POST',
|
||||||
|
fields: {
|
||||||
|
part_1: {
|
||||||
|
hidden: true,
|
||||||
|
value: {{ part.pk }},
|
||||||
},
|
},
|
||||||
reload: true,
|
part_2: {
|
||||||
|
label: '{% trans "Related Part" %}',
|
||||||
|
filters: {
|
||||||
|
exclude_id: {{ part.pk }},
|
||||||
|
exclude_related: {{ part.pk }},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focus: 'part_2',
|
||||||
|
title: '{% trans "Add Related Part" %}',
|
||||||
|
onSuccess: function() {
|
||||||
|
$('#related-parts-table').bootstrapTable('refresh');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1006,36 +892,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onPanelLoad("part-attachments", function() {
|
onPanelLoad("part-attachments", function() {
|
||||||
loadAttachmentTable(
|
loadAttachmentTable('{% url "api-part-attachment-list" %}', {
|
||||||
'{% url "api-part-attachment-list" %}',
|
|
||||||
{
|
|
||||||
filters: {
|
filters: {
|
||||||
part: {{ part.pk }},
|
part: {{ part.pk }},
|
||||||
},
|
},
|
||||||
onEdit: function(pk) {
|
|
||||||
var url = `/api/part/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
fields: {
|
||||||
filename: {},
|
part: {
|
||||||
comment: {},
|
value: {{ part.pk }},
|
||||||
},
|
hidden: true
|
||||||
title: '{% trans "Edit Attachment" %}',
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDelete: function(pk) {
|
|
||||||
var url = `/api/part/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
|
||||||
title: '{% trans "Delete Attachment" %}',
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
enableDragAndDrop(
|
enableDragAndDrop(
|
||||||
'#attachment-dropzone',
|
'#attachment-dropzone',
|
||||||
@ -1050,26 +917,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$("#new-attachment").click(function() {
|
|
||||||
|
|
||||||
constructForm(
|
|
||||||
'{% url "api-part-attachment-list" %}',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
attachment: {},
|
|
||||||
comment: {},
|
|
||||||
part: {
|
|
||||||
value: {{ part.pk }},
|
|
||||||
hidden: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
title: '{% trans "Add Attachment" %}',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% url 'part-index' as url %}
|
{% url 'part-index' as url %}
|
||||||
{% include "sidebar_link.html" with url=url text="Return to Parts" icon="fa-undo" %}
|
{% trans "Return to Parts" as text %}
|
||||||
|
{% include "sidebar_link.html" with url=url text=text icon="fa-undo" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -99,11 +99,14 @@
|
|||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
|
|
||||||
</h4>
|
|
||||||
<!-- Properties -->
|
<!-- Properties -->
|
||||||
<h4>
|
<table class='table table-striped table-condensed' id='part-info-table'>
|
||||||
<div id='part-properties' class='btn-group'>
|
<col width='25'>
|
||||||
|
<tr>
|
||||||
|
<td colspan='3' style='padding: 3px;'>
|
||||||
|
<div id='part-properties-wrapper' class='d-flex flex-wrap'>
|
||||||
|
<div id='part-properties' class='btn-group' role='group';'>
|
||||||
|
<h5>
|
||||||
{% if part.is_template %}
|
{% if part.is_template %}
|
||||||
 
|
 
|
||||||
<span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span>
|
<span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span>
|
||||||
@ -144,8 +147,23 @@
|
|||||||
{% trans 'Virtual' %}
|
{% trans 'Virtual' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
|
||||||
|
{% include "spacer.html" %}
|
||||||
|
|
||||||
|
<button type='button' class='btn btn-outline-secondary' data-bs-toggle='collapse' href='#collapse-part-details' role='button' id='toggle-details-button'>
|
||||||
|
{% trans "Show Part Details" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-info-circle'></span></td>
|
||||||
|
<td>{% trans "Description" %}</td>
|
||||||
|
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<!-- Part info messages -->
|
<!-- Part info messages -->
|
||||||
<div class='info-messages'>
|
<div class='info-messages'>
|
||||||
@ -157,7 +175,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock details %}
|
||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
<table class='table table-condensed table-striped'>
|
<table class='table table-condensed table-striped'>
|
||||||
@ -231,7 +249,118 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock details_right %}
|
||||||
|
|
||||||
|
{% block details_below %}
|
||||||
|
<!-- Part Details -->
|
||||||
|
<div class='collapse' id='collapse-part-details'>
|
||||||
|
<div class='row flex-wrap'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<!-- Details Table -->
|
||||||
|
<table class="table table-striped table-condensed">
|
||||||
|
<col width='25'>
|
||||||
|
{% if part.category %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-sitemap'></span></td>
|
||||||
|
<td>{% trans "Category" %}</td>
|
||||||
|
<td>
|
||||||
|
<a href='{% url "category-detail" part.category.pk %}'>{{ part.category.name }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if part.IPN %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-tag'></span></td>
|
||||||
|
<td>{% trans "IPN" %}</td>
|
||||||
|
<td>{{ part.IPN }}{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if part.revision %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-code-branch'></span></td>
|
||||||
|
<td>{% trans "Revision" %}</td>
|
||||||
|
<td>{{ part.revision }}{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if part.units %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{% trans "Units" %}</td>
|
||||||
|
<td>{{ part.units }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if part.minimum_stock %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-flag'></span></td>
|
||||||
|
<td>{% trans "Minimum stock level" %}</td>
|
||||||
|
<td>{{ part.minimum_stock }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if part.keywords %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-key'></span></td>
|
||||||
|
<td>{% trans "Keywords" %}</td>
|
||||||
|
<td>{{ part.keywords }}{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<table class="table table-striped table-condensed">
|
||||||
|
<col width='25'>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
<td>{% trans "Creation Date" %}</td>
|
||||||
|
<td>
|
||||||
|
{{ part.creation_date }}
|
||||||
|
{% if part.creation_user %}
|
||||||
|
<span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if part.trackable and part.getLatestSerialNumber %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
|
<td>{% trans "Latest Serial Number" %}</td>
|
||||||
|
<td>
|
||||||
|
{{ part.getLatestSerialNumber }}
|
||||||
|
<div class='btn-group float-right' role='group'>
|
||||||
|
<a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
|
||||||
|
<span class='fas fa-search'></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if part.default_location %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-search-location'></span></td>
|
||||||
|
<td>{% trans "Default Location" %}</td>
|
||||||
|
<td>
|
||||||
|
<a href='{% url "stock-location-detail" part.default_location.pk %}'>{{ part.default_location }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if part.default_supplier %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-building'></span></td>
|
||||||
|
<td>{% trans "Default Supplier" %}</td>
|
||||||
|
<td>{{ part.default_supplier }}</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 %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock details_below %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -439,4 +568,24 @@
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
// Callback function when the "part details" panel is shown
|
||||||
|
$('#collapse-part-details').on('show.bs.collapse', function() {
|
||||||
|
$('#toggle-details-button').html('{% trans "Hide Part Details" %}');
|
||||||
|
inventreeSave('show-part-details', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback function when the "part details" panel is hidden
|
||||||
|
$('#collapse-part-details').on('hide.bs.collapse', function() {
|
||||||
|
$('#toggle-details-button').html('{% trans "Show Part Details" %}');
|
||||||
|
inventreeSave('show-part-details', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inventreeLoad('show-part-details', false).toString() == 'true') {
|
||||||
|
$('#collapse-part-details').collapse('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#serial-number-search').click(function() {
|
||||||
|
findStockItemBySerialNumber({{ part.pk }});
|
||||||
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -5,34 +5,47 @@
|
|||||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||||
{% settings_value 'PART_SHOW_RELATED' as show_related %}
|
{% settings_value 'PART_SHOW_RELATED' as show_related %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label="part-details" text="Details" icon="fa-shapes" %}
|
{% trans "Parameters" as text %}
|
||||||
{% include "sidebar_item.html" with label="part-parameters" text="Parameters" icon="fa-th-list" %}
|
{% include "sidebar_item.html" with label="part-parameters" text=text icon="fa-th-list" %}
|
||||||
{% if part.is_template %}
|
{% if part.is_template %}
|
||||||
{% include "sidebar_item.html" with label="variants" text="Variants" icon="fa-shapes" %}
|
{% trans "Variants" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="variants" text=text icon="fa-shapes" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label="part-stock" text="Stock" icon="fa-boxes" %}
|
{% trans "Stock" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="part-stock" text=text icon="fa-boxes" %}
|
||||||
{% if part.assembly %}
|
{% if part.assembly %}
|
||||||
{% include "sidebar_item.html" with label="bom" text="Bill of Materials" icon="fa-list" %}
|
{% trans "Bill of Materials" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %}
|
||||||
{% if roles.build.view %}
|
{% if roles.build.view %}
|
||||||
{% include "sidebar_item.html" with label="build-orders" text="Build Orders" icon="fa-tools" %}
|
{% trans "Build Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.component %}
|
{% if part.component %}
|
||||||
{% include "sidebar_item.html" with label="used-in" text="Used In" icon="fa-layer-group" %}
|
{% trans "Used In" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="used-in" text=text icon="fa-layer-group" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label="pricing" text="Pricing" icon="fa-dollar-sign" %}
|
{% trans "Pricing" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
|
||||||
{% if part.purchaseable and roles.purchase_order.view %}
|
{% if part.purchaseable and roles.purchase_order.view %}
|
||||||
{% include "sidebar_item.html" with label="suppliers" text="Suppliers" icon="fa-building" %}
|
{% trans "Suppliers" as text %}
|
||||||
{% include "sidebar_item.html" with label="purchase-orders" text="Purchase Orders" icon="fa-shopping-cart" %}
|
{% include "sidebar_item.html" with label="suppliers" 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 part.salable and roles.sales_order.view %}
|
{% if part.salable and roles.sales_order.view %}
|
||||||
{% 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="sales-orders" text=text icon="fa-truck" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.trackable %}
|
{% if part.trackable %}
|
||||||
{% include "sidebar_item.html" with label="test-templates" text="Test Templates" icon="fa-vial" %}
|
{% trans "Test Templates" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_related %}
|
{% if show_related %}
|
||||||
{% include "sidebar_item.html" with label="related-parts" text="Related Parts" icon="fa-random" %}
|
{% trans "Related Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="related-parts" text=text icon="fa-random" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label="part-attachments" text="Attachments" icon="fa-paperclip" %}
|
{% trans "Attachments" as text %}
|
||||||
{% include "sidebar_item.html" with label="part-notes" text="Notes" icon="fa-clipboard" %}
|
{% include "sidebar_item.html" with label="part-attachments" text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="part-notes" text=text icon="fa-clipboard" %}
|
||||||
|
@ -925,7 +925,46 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
expected_code=200
|
expected_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
print("results:", len(response.data))
|
# Filter by "validated"
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'validated': True,
|
||||||
|
},
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be zero validated results
|
||||||
|
self.assertEqual(len(response.data), 0)
|
||||||
|
|
||||||
|
# Now filter by "not validated"
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'validated': False,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
# There should be at least one non-validated item
|
||||||
|
self.assertTrue(len(response.data) > 0)
|
||||||
|
|
||||||
|
# Now, let's validate an item
|
||||||
|
bom_item = BomItem.objects.first()
|
||||||
|
|
||||||
|
bom_item.validate_hash()
|
||||||
|
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'validated': True,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the expected response is returned
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertEqual(response.data[0]['pk'], bom_item.pk)
|
||||||
|
|
||||||
def test_get_bom_detail(self):
|
def test_get_bom_detail(self):
|
||||||
"""
|
"""
|
||||||
|
@ -5,7 +5,7 @@ from django.urls import reverse
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
from .models import Part, PartRelated
|
from .models import Part
|
||||||
|
|
||||||
|
|
||||||
class PartViewTestCase(TestCase):
|
class PartViewTestCase(TestCase):
|
||||||
@ -145,36 +145,6 @@ class PartDetailTest(PartViewTestCase):
|
|||||||
self.assertIn('streaming_content', dir(response))
|
self.assertIn('streaming_content', dir(response))
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedTests(PartViewTestCase):
|
|
||||||
|
|
||||||
def test_valid_create(self):
|
|
||||||
""" test creation of a related part """
|
|
||||||
|
|
||||||
# Test GET view
|
|
||||||
response = self.client.get(reverse('part-related-create'), {'part': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# Test POST view with valid form data
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 2},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": true', status_code=200)
|
|
||||||
|
|
||||||
# Try to create the same relationship with part_1 and part_2 pks reversed
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 2, 'part_2': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": false', status_code=200)
|
|
||||||
|
|
||||||
# Try to create part related to itself
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": false', status_code=200)
|
|
||||||
|
|
||||||
# Check final count
|
|
||||||
n = PartRelated.objects.all().count()
|
|
||||||
self.assertEqual(n, 1)
|
|
||||||
|
|
||||||
|
|
||||||
class PartQRTest(PartViewTestCase):
|
class PartQRTest(PartViewTestCase):
|
||||||
""" Tests for the Part QR Code AJAX view """
|
""" Tests for the Part QR Code AJAX view """
|
||||||
|
|
||||||
|
@ -12,10 +12,6 @@ from django.conf.urls import url, include
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
part_related_urls = [
|
|
||||||
url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'),
|
|
||||||
url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'),
|
|
||||||
]
|
|
||||||
|
|
||||||
sale_price_break_urls = [
|
sale_price_break_urls = [
|
||||||
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
||||||
@ -96,9 +92,6 @@ part_urls = [
|
|||||||
# Part category
|
# Part category
|
||||||
url(r'^category/', include(category_urls)),
|
url(r'^category/', include(category_urls)),
|
||||||
|
|
||||||
# Part related
|
|
||||||
url(r'^related-parts/', include(part_related_urls)),
|
|
||||||
|
|
||||||
# Part price breaks
|
# Part price breaks
|
||||||
url(r'^sale-price/', include(sale_price_break_urls)),
|
url(r'^sale-price/', include(sale_price_break_urls)),
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import io
|
|||||||
from rapidfuzz import fuzz
|
from rapidfuzz import fuzz
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
from .models import PartCategory, Part, PartRelated
|
from .models import PartCategory, Part
|
||||||
from .models import PartParameterTemplate
|
from .models import PartParameterTemplate
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
@ -85,75 +85,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedCreate(AjaxCreateView):
|
|
||||||
""" View for creating a new PartRelated object
|
|
||||||
|
|
||||||
- The view only makes sense if a Part object is passed to it
|
|
||||||
"""
|
|
||||||
model = PartRelated
|
|
||||||
form_class = part_forms.CreatePartRelatedForm
|
|
||||||
ajax_form_title = _("Add Related Part")
|
|
||||||
ajax_template_name = "modal_form.html"
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
""" Set parent part as part_1 field """
|
|
||||||
|
|
||||||
initials = {}
|
|
||||||
|
|
||||||
part_id = self.request.GET.get('part', None)
|
|
||||||
|
|
||||||
if part_id:
|
|
||||||
try:
|
|
||||||
initials['part_1'] = Part.objects.get(pk=part_id)
|
|
||||||
except (Part.DoesNotExist, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
""" Create a form to upload a new PartRelated
|
|
||||||
|
|
||||||
- Hide the 'part_1' field (parent part)
|
|
||||||
- Display parts which are not yet related
|
|
||||||
"""
|
|
||||||
|
|
||||||
form = super(AjaxCreateView, self).get_form()
|
|
||||||
|
|
||||||
form.fields['part_1'].widget = HiddenInput()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get parent part
|
|
||||||
parent_part = self.get_initial()['part_1']
|
|
||||||
# Get existing related parts
|
|
||||||
related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()]
|
|
||||||
|
|
||||||
# Build updated choice list excluding
|
|
||||||
# - parts already related to parent part
|
|
||||||
# - the parent part itself
|
|
||||||
updated_choices = []
|
|
||||||
for choice in form.fields["part_2"].choices:
|
|
||||||
if (choice[0] not in related_parts) and (choice[0] != parent_part.pk):
|
|
||||||
updated_choices.append(choice)
|
|
||||||
|
|
||||||
# Update choices for related part
|
|
||||||
form.fields['part_2'].choices = updated_choices
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedDelete(AjaxDeleteView):
|
|
||||||
""" View for deleting a PartRelated object """
|
|
||||||
|
|
||||||
model = PartRelated
|
|
||||||
ajax_form_title = _("Delete Related Part")
|
|
||||||
context_object_name = "related"
|
|
||||||
|
|
||||||
# Explicit role requirement
|
|
||||||
role_required = 'part.change'
|
|
||||||
|
|
||||||
|
|
||||||
class PartSetCategory(AjaxUpdateView):
|
class PartSetCategory(AjaxUpdateView):
|
||||||
""" View for settings the part category for multiple parts at once """
|
""" View for settings the part category for multiple parts at once """
|
||||||
|
|
||||||
@ -508,10 +439,14 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
line['price_part'] = stock_item.supplier_part.unit_pricing
|
line['price_part'] = stock_item.supplier_part.unit_pricing
|
||||||
|
|
||||||
# set date for graph labels
|
# set date for graph labels
|
||||||
if stock_item.purchase_order:
|
if stock_item.purchase_order and stock_item.purchase_order.issue_date:
|
||||||
line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y')
|
line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y')
|
||||||
else:
|
elif stock_item.tracking_info.count() > 0:
|
||||||
line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
|
line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
|
||||||
|
else:
|
||||||
|
# Not enough information
|
||||||
|
continue
|
||||||
|
|
||||||
price_history.append(line)
|
price_history.append(line)
|
||||||
|
|
||||||
ctx['price_history'] = price_history
|
ctx['price_history'] = price_history
|
||||||
|
@ -313,7 +313,7 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
# Serial number filtering
|
# Serial number filtering
|
||||||
serial_gte = rest_filters.NumberFilter(label='Serial number GTE', field_name='serial', lookup_expr='gte')
|
serial_gte = rest_filters.NumberFilter(label='Serial number GTE', field_name='serial', lookup_expr='gte')
|
||||||
serial_lte = rest_filters.NumberFilter(label='Serial number LTE', field_name='serial', lookup_expr='lte')
|
serial_lte = rest_filters.NumberFilter(label='Serial number LTE', field_name='serial', lookup_expr='lte')
|
||||||
serial = rest_filters.NumberFilter(label='Serial number', field_name='serial', lookup_expr='exact')
|
serial = rest_filters.CharFilter(label='Serial number', field_name='serial', lookup_expr='exact')
|
||||||
|
|
||||||
serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
|
serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
|
||||||
|
|
||||||
@ -703,6 +703,18 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
except (ValueError, StockItem.DoesNotExist):
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Filter by "part tree" - only allow parts within a given variant tree
|
||||||
|
part_tree = params.get('part_tree', None)
|
||||||
|
|
||||||
|
if part_tree is not None:
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(pk=part_tree)
|
||||||
|
|
||||||
|
if part.tree_id is not None:
|
||||||
|
queryset = queryset.filter(part__tree_id=part.tree_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# Filter by 'allocated' parts?
|
# Filter by 'allocated' parts?
|
||||||
allocated = params.get('allocated', None)
|
allocated = params.get('allocated', None)
|
||||||
|
|
||||||
|
25
InvenTree/stock/migrations/0070_auto_20211128_0151.py
Normal file
25
InvenTree/stock/migrations/0070_auto_20211128_0151.py
Normal file
@ -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 = [
|
||||||
|
('stock', '0069_auto_20211109_2347'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stockitemattachment',
|
||||||
|
name='link',
|
||||||
|
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitemattachment',
|
||||||
|
name='attachment',
|
||||||
|
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||||
|
),
|
||||||
|
]
|
@ -7,7 +7,6 @@ Stock database model definitions
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError, FieldError
|
from django.core.exceptions import ValidationError, FieldError
|
||||||
@ -39,6 +38,7 @@ import label.models
|
|||||||
from InvenTree.status_codes import StockStatus, StockHistoryCode
|
from InvenTree.status_codes import StockStatus, StockHistoryCode
|
||||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
||||||
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
||||||
|
from InvenTree.serializers import extract_int
|
||||||
|
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
@ -236,17 +236,7 @@ class StockItem(MPTTModel):
|
|||||||
serial_int = 0
|
serial_int = 0
|
||||||
|
|
||||||
if serial is not None:
|
if serial is not None:
|
||||||
|
serial_int = extract_int(str(serial))
|
||||||
serial = str(serial)
|
|
||||||
|
|
||||||
# Look at the start of the string - can it be "integerized"?
|
|
||||||
result = re.match(r'^(\d+)', serial)
|
|
||||||
|
|
||||||
if result and len(result.groups()) == 1:
|
|
||||||
try:
|
|
||||||
serial_int = int(result.groups()[0])
|
|
||||||
except:
|
|
||||||
serial_int = 0
|
|
||||||
|
|
||||||
self.serial_int = serial_int
|
self.serial_int = serial_int
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ from company.serializers import SupplierPartSerializer
|
|||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
from InvenTree.serializers import InvenTreeDecimalField
|
from InvenTree.serializers import InvenTreeDecimalField, extract_int
|
||||||
|
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
@ -73,6 +73,11 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
'uid',
|
'uid',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def validate_serial(self, value):
|
||||||
|
if extract_int(value) > 2147483647:
|
||||||
|
raise serializers.ValidationError('serial is to to big')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||||
""" Serializer for a StockItem:
|
""" Serializer for a StockItem:
|
||||||
@ -420,8 +425,6 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
|
|||||||
|
|
||||||
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
|
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
|
||||||
|
|
||||||
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True)
|
|
||||||
|
|
||||||
# TODO: Record the uploading user when creating or updating an attachment!
|
# TODO: Record the uploading user when creating or updating an attachment!
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -432,6 +435,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
|
|||||||
'stock_item',
|
'stock_item',
|
||||||
'attachment',
|
'attachment',
|
||||||
'filename',
|
'filename',
|
||||||
|
'link',
|
||||||
'comment',
|
'comment',
|
||||||
'upload_date',
|
'upload_date',
|
||||||
'user',
|
'user',
|
||||||
|
@ -221,55 +221,16 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
loadAttachmentTable(
|
loadAttachmentTable('{% url "api-stock-attachment-list" %}', {
|
||||||
'{% url "api-stock-attachment-list" %}',
|
|
||||||
{
|
|
||||||
filters: {
|
filters: {
|
||||||
stock_item: {{ item.pk }},
|
stock_item: {{ item.pk }},
|
||||||
},
|
},
|
||||||
onEdit: function(pk) {
|
|
||||||
var url = `/api/stock/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
fields: {
|
||||||
filename: {},
|
|
||||||
comment: {},
|
|
||||||
},
|
|
||||||
title: '{% trans "Edit Attachment" %}',
|
|
||||||
onSuccess: reloadAttachmentTable
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDelete: function(pk) {
|
|
||||||
var url = `/api/stock/attachment/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
|
||||||
title: '{% trans "Delete Attachment" %}',
|
|
||||||
onSuccess: reloadAttachmentTable,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
$("#new-attachment").click(function() {
|
|
||||||
|
|
||||||
constructForm(
|
|
||||||
'{% url "api-stock-attachment-list" %}',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
attachment: {},
|
|
||||||
comment: {},
|
|
||||||
stock_item: {
|
stock_item: {
|
||||||
value: {{ item.pk }},
|
value: {{ item.pk }},
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
|
||||||
},
|
|
||||||
reload: true,
|
|
||||||
title: '{% trans "Add Attachment" %}',
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loadStockTestResultsTable(
|
loadStockTestResultsTable(
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
{% trans "Stock Item" %}: {{ item.part.full_name}}
|
{% trans "Stock Item" %}: {{ item.part.full_name}}
|
||||||
{% endblock %}
|
{% endblock heading %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
|
|
||||||
@ -100,7 +100,9 @@
|
|||||||
<!-- Edit stock item -->
|
<!-- Edit stock item -->
|
||||||
{% if roles.stock.change and not item.is_building %}
|
{% if roles.stock.change and not item.is_building %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-tools'></span> <span class='caret'></span></button>
|
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
|
||||||
|
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||||
|
</button>
|
||||||
<ul class='dropdown-menu' role='menu'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
{% if item.part.can_convert %}
|
{% if item.part.can_convert %}
|
||||||
<li><a class='dropdown-item' href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li>
|
||||||
@ -118,38 +120,101 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock actions %}
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<img class='part-thumb' {% if item.part.image %}src="{{ item.part.image.url }}"{% else %}src="{% static 'img/blank_image.png' %}"{% endif %}/>
|
<img class='part-thumb' {% if item.part.image %}src="{{ item.part.image.url }}"{% else %}src="{% static 'img/blank_image.png' %}"{% endif %}/>
|
||||||
{% endblock %}
|
{% endblock thumbnail %}
|
||||||
|
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<col width='25'>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-shapes'></span></td>
|
||||||
|
<td>{% trans "Base Part" %}</td>
|
||||||
|
<td>
|
||||||
|
{% if roles.part.view %}
|
||||||
|
<a href="{% url 'part-detail' item.part.id %}">
|
||||||
|
{% endif %}
|
||||||
|
{{ item.part.full_name }}
|
||||||
|
{% if roles.part.view %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if item.serialized %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
|
<td>{% trans "Serial Number" %}</td>
|
||||||
|
<td>
|
||||||
|
{{ item.serial }}
|
||||||
|
<div class='btn-group float-right' role='group'>
|
||||||
|
{% if previous %}
|
||||||
|
<a class="btn btn-small btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}" title='{% trans "Navigate to previous serial number" %}'>
|
||||||
|
<span class='fas fa-angle-left'></span>
|
||||||
|
<small>{{ previous.serial }}</small>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
|
||||||
|
<span class='fas fa-search'></span>
|
||||||
|
</a>
|
||||||
|
{% if next %}
|
||||||
|
<a class="btn btn-small btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}" title='{% trans "Navigate to next serial number" %}'>
|
||||||
|
<small>{{ next.serial }}</small>
|
||||||
|
<span class='fas fa-angle-right'></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>{% trans "Quantity" %}</td>
|
||||||
|
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-info'></span></td>
|
||||||
|
<td>{% trans "Status" %}</td>
|
||||||
|
<td>{% stock_status_label item.status %}</td>
|
||||||
|
</tr>
|
||||||
|
{% if item.expiry_date %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
|
||||||
|
<td>{% trans "Expiry Date" %}</td>
|
||||||
|
<td>
|
||||||
|
{{ item.expiry_date }}
|
||||||
|
{% if item.is_expired %}
|
||||||
|
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger badge-right'>{% trans "Expired" %}</span>
|
||||||
|
{% elif item.is_stale %}
|
||||||
|
<span title='{% blocktrans %}This StockItem expires on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-warning badge-right'>{% trans "Stale" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
<td>{% trans "Last Updated" %}</td>
|
||||||
|
<td>{{ item.updated }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
<td>{% trans "Last Stocktake" %}</td>
|
||||||
|
{% if item.stocktake_date %}
|
||||||
|
<td>{{ item.stocktake_date }} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
|
||||||
|
{% else %}
|
||||||
|
<td><em>{% trans "No stocktake performed" %}</em></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||||
{% if owner_control.value == "True" %}
|
{% if owner_control.value == "True" %}
|
||||||
{% authorized_owners item.owner as owners %}
|
{% authorized_owners item.owner as owners %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h4>
|
|
||||||
{% if item.is_expired %}
|
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Expired" %}</span>
|
|
||||||
{% else %}
|
|
||||||
{% if roles.stock.change %}
|
|
||||||
<a href='#' id='stock-edit-status'>
|
|
||||||
{% endif %}
|
|
||||||
{% stock_status_label item.status large=True %}
|
|
||||||
{% if roles.stock.change %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.is_stale %}
|
|
||||||
<span class='badge rounded-pill bg-warning'>{% trans "Stale" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class='info-messages'>
|
<div class='info-messages'>
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||||
@ -214,49 +279,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock details %}
|
||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
<table class="table table-striped">
|
<table class="table table-striped table-condensed">
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-shapes'></span></td>
|
|
||||||
<td>{% trans "Base Part" %}</td>
|
|
||||||
<td>
|
|
||||||
{% if roles.part.view %}
|
|
||||||
<a href="{% url 'part-detail' item.part.id %}">
|
|
||||||
{% endif %}
|
|
||||||
{{ item.part.full_name }}
|
|
||||||
{% if roles.part.view %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if item.serialized %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
|
||||||
<td>{% trans "Serial Number" %}</td>
|
|
||||||
<td>
|
|
||||||
{% if previous %}
|
|
||||||
<a class="btn btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}">
|
|
||||||
<small>{{ previous.serial }}</small> ‹
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{{ item.serial }}
|
|
||||||
{% if next %}
|
|
||||||
<a class="btn btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}">
|
|
||||||
› <small>{{ next.serial }}</small>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>{% trans "Quantity" %}</td>
|
|
||||||
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.customer %}
|
{% if item.customer %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-user-tie'></span></td>
|
<td><span class='fas fa-user-tie'></span></td>
|
||||||
@ -376,39 +404,6 @@
|
|||||||
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
|
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.expiry_date %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
|
|
||||||
<td>{% trans "Expiry Date" %}</td>
|
|
||||||
<td>
|
|
||||||
{{ item.expiry_date }}
|
|
||||||
{% if item.is_expired %}
|
|
||||||
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger'>{% trans "Expired" %}</span>
|
|
||||||
{% elif item.is_stale %}
|
|
||||||
<span title='{% blocktrans %}This StockItem expires on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-warning'>{% trans "Stale" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
|
||||||
<td>{% trans "Last Updated" %}</td>
|
|
||||||
<td>{{ item.updated }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
|
||||||
<td>{% trans "Last Stocktake" %}</td>
|
|
||||||
{% if item.stocktake_date %}
|
|
||||||
<td>{{ item.stocktake_date }} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
|
|
||||||
{% else %}
|
|
||||||
<td><em>{% trans "No stocktake performed" %}</em></td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class='fas fa-info'></span></td>
|
|
||||||
<td>{% trans "Status" %}</td>
|
|
||||||
<td>{% stock_status_label item.status %}</td>
|
|
||||||
</tr>
|
|
||||||
{% if item.hasRequiredTests %}
|
{% if item.hasRequiredTests %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-vial'></span></td>
|
<td><span class='fas fa-vial'></span></td>
|
||||||
@ -433,6 +428,7 @@
|
|||||||
$("#stock-serialize").click(function() {
|
$("#stock-serialize").click(function() {
|
||||||
|
|
||||||
serializeStockItem({{ item.pk }}, {
|
serializeStockItem({{ item.pk }}, {
|
||||||
|
part: {{ item.part.pk }},
|
||||||
reload: true,
|
reload: true,
|
||||||
data: {
|
data: {
|
||||||
quantity: {{ item.quantity }},
|
quantity: {{ item.quantity }},
|
||||||
@ -603,4 +599,8 @@ $("#stock-return-from-customer").click(function() {
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
$('#serial-number-search').click(function() {
|
||||||
|
findStockItemBySerialNumber({{ item.part.pk }});
|
||||||
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user