mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-17 18:02:51 +00:00
merge
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
name: Check Translations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- l10
|
||||
pull_request:
|
||||
branches:
|
||||
- l10
|
||||
|
||||
jobs:
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Test Translations
|
||||
run: invoke translate
|
||||
- name: Check Migration Files
|
||||
run: python3 ci/check_migration_files.py
|
||||
@@ -23,11 +23,13 @@ jobs:
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
steps:
|
||||
- name: Install node.js
|
||||
uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npm install
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@@ -41,7 +43,6 @@ jobs:
|
||||
invoke static
|
||||
- name: Check HTML Files
|
||||
run: |
|
||||
npm install markuplint
|
||||
npx markuplint InvenTree/build/templates/build/*.html
|
||||
npx markuplint InvenTree/company/templates/company/*.html
|
||||
npx markuplint InvenTree/order/templates/order/*.html
|
||||
|
||||
@@ -23,11 +23,13 @@ jobs:
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
steps:
|
||||
- name: Install node.js
|
||||
uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npm install
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@@ -45,6 +47,5 @@ jobs:
|
||||
python check_js_templates.py
|
||||
- name: Lint Javascript Files
|
||||
run: |
|
||||
npm install eslint eslint-config-google
|
||||
invoke render-js-files
|
||||
npx eslint js_tmp/*.js
|
||||
@@ -78,5 +78,4 @@ locale_stats.json
|
||||
|
||||
# node.js
|
||||
package-lock.json
|
||||
package.json
|
||||
node_modules/
|
||||
@@ -2,8 +2,7 @@
|
||||
Pull rendered copies of the templated
|
||||
"""
|
||||
|
||||
from django.http import response
|
||||
from django.test import TestCase, testcases
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
import os
|
||||
|
||||
@@ -21,7 +21,8 @@ from django.dispatch import receiver
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
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')
|
||||
@@ -48,6 +49,9 @@ class ReferenceIndexingMixin(models.Model):
|
||||
"""
|
||||
A mixin for keeping track of numerical copies of the "reference" field.
|
||||
|
||||
!!DANGER!! always add `ReferenceIndexingSerializerMixin`to all your models serializers to
|
||||
ensure the reference field is not too big
|
||||
|
||||
Here, we attempt to convert a "reference" field value (char) to an integer,
|
||||
for performing fast natural sorting.
|
||||
|
||||
@@ -68,6 +72,12 @@ class ReferenceIndexingMixin(models.Model):
|
||||
|
||||
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
|
||||
ref_int = 0
|
||||
|
||||
@@ -80,21 +90,21 @@ class ReferenceIndexingMixin(models.Model):
|
||||
ref_int = int(ref)
|
||||
except:
|
||||
ref_int = 0
|
||||
|
||||
self.reference_int = ref_int
|
||||
|
||||
reference_int = models.IntegerField(default=0)
|
||||
return ref_int
|
||||
|
||||
|
||||
class InvenTreeAttachment(models.Model):
|
||||
""" Provides an abstracted class for managing file attachments.
|
||||
|
||||
An attachment can be either an uploaded file, or an external URL
|
||||
|
||||
Attributes:
|
||||
attachment: File
|
||||
comment: String descriptor for the attachment
|
||||
user: User associated with file upload
|
||||
upload_date: Date the file was uploaded
|
||||
"""
|
||||
|
||||
def getSubdir(self):
|
||||
"""
|
||||
Return the subdirectory under which attachments should be stored.
|
||||
@@ -103,11 +113,32 @@ class InvenTreeAttachment(models.Model):
|
||||
|
||||
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):
|
||||
if self.attachment is not None:
|
||||
return os.path.basename(self.attachment.name)
|
||||
else:
|
||||
return str(self.link)
|
||||
|
||||
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
|
||||
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'))
|
||||
|
||||
@@ -123,7 +154,10 @@ class InvenTreeAttachment(models.Model):
|
||||
|
||||
@property
|
||||
def basename(self):
|
||||
if self.attachment:
|
||||
return os.path.basename(self.attachment.name)
|
||||
else:
|
||||
return None
|
||||
|
||||
@basename.setter
|
||||
def basename(self, fn):
|
||||
|
||||
@@ -16,6 +16,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
from djmoney.contrib.django_rest_framework.fields import MoneyField
|
||||
from djmoney.money import Money
|
||||
@@ -27,6 +28,8 @@ from rest_framework.fields import empty
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.serializers import DecimalField
|
||||
|
||||
from .models import extract_int
|
||||
|
||||
|
||||
class InvenTreeMoneySerializer(MoneyField):
|
||||
"""
|
||||
@@ -239,20 +242,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
|
||||
class ReferenceIndexingSerializerMixin():
|
||||
"""
|
||||
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
|
||||
|
||||
The only real addition here is that we support "renaming" of the attachment file.
|
||||
This serializer mixin ensures the the reference is not to big / small
|
||||
for the BigIntegerField
|
||||
"""
|
||||
|
||||
# The 'filename' field must be present in the serializer
|
||||
filename = serializers.CharField(
|
||||
label=_('Filename'),
|
||||
required=False,
|
||||
source='basename',
|
||||
allow_blank=False,
|
||||
)
|
||||
def validate_reference(self, value):
|
||||
if extract_int(value) > models.BigIntegerField.MAX_BIGINT:
|
||||
raise serializers.ValidationError('reference is to to big')
|
||||
return value
|
||||
|
||||
|
||||
class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||
@@ -284,6 +282,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||
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):
|
||||
"""
|
||||
Custom image serializer.
|
||||
|
||||
@@ -26,6 +26,7 @@ import moneyed
|
||||
import yaml
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.messages import constants as messages
|
||||
import django.conf.locale
|
||||
|
||||
|
||||
def _is_true(x):
|
||||
@@ -256,7 +257,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'user_sessions', # db user sessions
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
@@ -304,7 +305,7 @@ INSTALLED_APPS = [
|
||||
|
||||
MIDDLEWARE = CONFIG.get('middleware', [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'user_sessions.middleware.SessionMiddleware', # db user sessions
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
@@ -634,6 +635,12 @@ if _cache_host:
|
||||
# as well
|
||||
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
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
||||
@@ -673,7 +680,7 @@ LANGUAGES = [
|
||||
('el', _('Greek')),
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('es-mx', _('Spanish (Mexican')),
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('fr', _('French')),
|
||||
('he', _('Hebrew')),
|
||||
('it', _('Italian')),
|
||||
@@ -691,6 +698,25 @@ LANGUAGES = [
|
||||
('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 = CONFIG.get(
|
||||
'currencies',
|
||||
|
||||
@@ -781,6 +781,7 @@ input[type="submit"] {
|
||||
.btn-small {
|
||||
padding: 3px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
|
||||
@@ -38,6 +38,7 @@ from rest_framework.documentation import include_docs_urls
|
||||
from .views import auth_request
|
||||
from .views import IndexView, SearchView, DatabaseStatsView
|
||||
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
|
||||
from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView
|
||||
from .views import CurrencyRefreshView
|
||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||
from .views import DynamicJsView
|
||||
@@ -157,6 +158,10 @@ frontendpatterns = [
|
||||
url(r'^search/', SearchView.as_view(), name='search'),
|
||||
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
|
||||
# overrides of urlpatterns
|
||||
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 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
|
||||
|
||||
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
|
||||
- Adds the ability to filter BomItem API by "uses" field
|
||||
- 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.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.timezone import now
|
||||
from django.shortcuts import redirect
|
||||
from django.conf import settings
|
||||
|
||||
@@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm
|
||||
from allauth.account.models import EmailAddress
|
||||
from allauth.account.views import EmailView, PasswordResetFromKeyView
|
||||
from allauth.socialaccount.views import ConnectionsView
|
||||
from user_sessions.views import SessionDeleteView, SessionDeleteOtherView
|
||||
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
|
||||
@@ -733,6 +735,10 @@ class SettingsView(TemplateView):
|
||||
ctx["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
|
||||
|
||||
|
||||
@@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView):
|
||||
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):
|
||||
"""
|
||||
POST endpoint to refresh / update exchange rates
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.5 on 2021-11-28 01:51
|
||||
|
||||
import InvenTree.fields
|
||||
import InvenTree.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('build', '0032_auto_20211014_0632'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='buildorderattachment',
|
||||
name='link',
|
||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='buildorderattachment',
|
||||
name='attachment',
|
||||
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.5 on 2021-12-01 21:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('build', '0033_auto_20211128_0151'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='reference_int',
|
||||
field=models.BigIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -16,7 +16,7 @@ from rest_framework import serializers
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
|
||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
|
||||
from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
|
||||
|
||||
import InvenTree.helpers
|
||||
from InvenTree.serializers import InvenTreeDecimalField
|
||||
@@ -32,7 +32,7 @@ from users.serializers import OwnerSerializer
|
||||
from .models import Build, BuildItem, BuildOrderAttachment
|
||||
|
||||
|
||||
class BuildSerializer(InvenTreeModelSerializer):
|
||||
class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializes a Build object
|
||||
"""
|
||||
@@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
Serializer for a BuildAttachment
|
||||
"""
|
||||
|
||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = BuildOrderAttachment
|
||||
|
||||
@@ -525,6 +523,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'pk',
|
||||
'build',
|
||||
'attachment',
|
||||
'link',
|
||||
'filename',
|
||||
'comment',
|
||||
'upload_date',
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% block breadcrumbs %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class="part-thumb"
|
||||
@@ -21,7 +21,7 @@ src="{{ build.part.image.url }}"
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}/>
|
||||
{% endblock %}
|
||||
{% endblock thumbnail %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Build Order" %} {{ build }}
|
||||
@@ -66,11 +66,23 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock actions %}
|
||||
|
||||
{% 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'>
|
||||
{% if build.sales_order %}
|
||||
@@ -114,11 +126,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
|
||||
{% block details_right %}
|
||||
<table class='table table-striped table-condensed'>
|
||||
<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>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Quantity" %}</td>
|
||||
|
||||
@@ -431,53 +431,17 @@ enableDragAndDrop(
|
||||
}
|
||||
);
|
||||
|
||||
// Callback for creating a new attachment
|
||||
$('#new-attachment').click(function() {
|
||||
|
||||
constructForm('{% url "api-build-attachment-list" %}', {
|
||||
loadAttachmentTable('{% url "api-build-attachment-list" %}', {
|
||||
filters: {
|
||||
build: {{ build.pk }},
|
||||
},
|
||||
fields: {
|
||||
attachment: {},
|
||||
comment: {},
|
||||
build: {
|
||||
value: {{ build.pk }},
|
||||
hidden: true,
|
||||
}
|
||||
},
|
||||
method: 'POST',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Add Attachment" %}',
|
||||
});
|
||||
});
|
||||
|
||||
loadAttachmentTable(
|
||||
'{% url "api-build-attachment-list" %}',
|
||||
{
|
||||
filters: {
|
||||
build: {{ build.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/build/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
filename: {},
|
||||
comment: {},
|
||||
},
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Edit Attachment" %}',
|
||||
});
|
||||
},
|
||||
onDelete: function(pk) {
|
||||
|
||||
constructForm(`/api/build/attachment/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
||||
title: '{% trans "Delete Attachment" %}',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#edit-notes').click(function() {
|
||||
constructForm('{% url "api-build-detail" build.pk %}', {
|
||||
|
||||
@@ -2,14 +2,21 @@
|
||||
{% load static %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% include "sidebar_item.html" with label='completed' text="Completed Items" icon="fa-boxes" %}
|
||||
{% include "sidebar_item.html" with label='children' text="Child Build Orders" icon="fa-sitemap" %}
|
||||
{% include "sidebar_item.html" with label='attachments' text="Attachments" icon="fa-paperclip" %}
|
||||
{% include "sidebar_item.html" with label='notes' text="Notes" icon="fa-clipboard" %}
|
||||
{% trans "Completed Items" as text %}
|
||||
{% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
|
||||
{% trans "Child Build Orders" as text %}
|
||||
{% 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():
|
||||
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)
|
||||
elif cls.validator_is_int(validator):
|
||||
try:
|
||||
@@ -538,6 +540,19 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
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():
|
||||
"""build up group tuple for settings based on gour choices"""
|
||||
|
||||
@@ -45,6 +45,18 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
||||
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -19,21 +19,26 @@
|
||||
{% include "admin_button.html" with url=url %}
|
||||
{% endif %}
|
||||
{% 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'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.company.change_company %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='company-edit' title='{% trans "Edit company information" %}'>
|
||||
<span class='fas fa-edit icon-green'/>
|
||||
<button id='company-edit-actions' title='{% trans "Company 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'>
|
||||
{% 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 %}
|
||||
{% if perms.company.delete_company %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='company-delete' title='{% trans "Delete Company" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
</button>
|
||||
<li><a class='dropdown-item' href='#' id='company-delete' title='{% trans "Delete company" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Company" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
{% endblock actions %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<div class='dropzone part-thumb-container' id='company-thumb'>
|
||||
@@ -56,7 +61,29 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@@ -110,22 +137,6 @@
|
||||
<td>{{ company.contact }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ InvenTree | {% trans "Manufacturer Part" %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include "company/manufacturer_part_sidebar.html" %}
|
||||
{% endblock %}
|
||||
{% endblock sidebar %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<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>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
<h4>
|
||||
{% trans "Manufacturer Part" %}: {{ part.part.full_name }}
|
||||
</h4>
|
||||
{% endblock %}
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
{% if user.is_staff and perms.company.change_company %}
|
||||
@@ -46,7 +46,7 @@ InvenTree | {% trans "Manufacturer Part" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock actions %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class='part-thumb'
|
||||
@@ -55,15 +55,11 @@ src='{{ part.part.image.url }}'
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}/>
|
||||
{% endblock %}
|
||||
{% endblock thumbnail %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block details_right %}
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<table class='table table-striped table-condensed'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Manufacturer" %}</td>
|
||||
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td></tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "MPN" %}</td>
|
||||
<td>{{ part.MPN }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endblock details_right %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
|
||||
@@ -2,5 +2,7 @@
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% include "sidebar_item.html" with label='parameters' text="Parameters" icon="fa-th-list" %}
|
||||
{% include "sidebar_item.html" with label='supplier-parts' text="Supplier Parts" icon="fa-building" %}
|
||||
{% trans "Parameters" as text %}
|
||||
{% 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 %}
|
||||
|
||||
{% 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 %}
|
||||
{% if company.is_supplier %}
|
||||
{% include "sidebar_item.html" with label='supplier-parts' text="Supplied Parts" icon="fa-building" %}
|
||||
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
|
||||
{% trans "Supplied Parts" as text %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% if company.is_customer %}
|
||||
{% include "sidebar_item.html" with label='sales-orders' text="Sales Orders" icon="fa-truck" %}
|
||||
{% include "sidebar_item.html" with label='assigned-stock' text="Assigned Stock Items" icon="fa-sign-out-alt" %}
|
||||
{% trans "Sales Orders" as text %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% inventree_title %} | {% trans "Supplier Part" %}
|
||||
{% endblock %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include "company/supplier_part_sidebar.html" %}
|
||||
{% endblock %}
|
||||
{% endblock sidebar %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<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>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
<h4>
|
||||
{% trans "Supplier Part" %}: {{ part.SKU }}
|
||||
</h4>
|
||||
{% endblock %}
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
{% if user.is_staff and perms.company.change_company %}
|
||||
@@ -43,7 +43,7 @@
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock actions %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class='part-thumb'
|
||||
@@ -56,15 +56,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
|
||||
{% block details %}
|
||||
|
||||
<p>
|
||||
{{ part.part.full_name }}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block details_right %}
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<table class='table table-striped table-condensed'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{% endblock details %}
|
||||
|
||||
{% block details_right %}
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
@@ -127,6 +120,13 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{{ part.note }}{% 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 %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% include "sidebar_item.html" with label='stock' text="Stock Items" icon="fa-boxes" %}
|
||||
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
|
||||
{% include "sidebar_item.html" with label='pricing' text="Supplier Part Pricing" icon="fa-dollar-sign" %}
|
||||
{% trans "Stock Items" as text %}
|
||||
{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
|
||||
{% 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.
+3285
-3079
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
+3656
-3277
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3321
-3115
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3241
-3035
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3079
-2873
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3935
-3728
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3067
-2861
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3072
-2866
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3079
-2873
File diff suppressed because it is too large
Load Diff
+1387
-1151
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3074
-2868
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3066
-2860
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3128
-2922
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3093
-2887
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3089
-2883
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.2.5 on 2021-11-28 01:51
|
||||
|
||||
import InvenTree.fields
|
||||
import InvenTree.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0052_auto_20211014_0631'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchaseorderattachment',
|
||||
name='link',
|
||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='salesorderattachment',
|
||||
name='link',
|
||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderattachment',
|
||||
name='attachment',
|
||||
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesorderattachment',
|
||||
name='attachment',
|
||||
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.5 on 2021-12-01 21:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0053_auto_20211128_0151'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorder',
|
||||
name='reference_int',
|
||||
field=models.BigIntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='salesorder',
|
||||
name='reference_int',
|
||||
field=models.BigIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -17,16 +17,16 @@ from rest_framework.serializers import ValidationError
|
||||
|
||||
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 InvenTreeModelSerializer
|
||||
from InvenTree.serializers import InvenTreeDecimalField
|
||||
from InvenTree.serializers import InvenTreeMoneySerializer
|
||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
||||
|
||||
from InvenTree.serializers import ReferenceIndexingSerializerMixin
|
||||
from InvenTree.status_codes import StockStatus
|
||||
|
||||
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
||||
|
||||
from part.serializers import PartBriefSerializer
|
||||
|
||||
import stock.models
|
||||
@@ -37,10 +37,10 @@ from .models import PurchaseOrderAttachment, SalesOrderAttachment
|
||||
from .models import SalesOrder, SalesOrderLineItem
|
||||
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 """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -86,6 +86,8 @@ class POSerializer(InvenTreeModelSerializer):
|
||||
|
||||
reference = serializers.CharField(required=True)
|
||||
|
||||
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrder
|
||||
|
||||
@@ -100,6 +102,7 @@ class POSerializer(InvenTreeModelSerializer):
|
||||
'overdue',
|
||||
'reference',
|
||||
'responsible',
|
||||
'responsible_detail',
|
||||
'supplier',
|
||||
'supplier_detail',
|
||||
'supplier_reference',
|
||||
@@ -374,8 +377,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
Serializers for the PurchaseOrderAttachment model
|
||||
"""
|
||||
|
||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrderAttachment
|
||||
|
||||
@@ -383,6 +384,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'pk',
|
||||
'order',
|
||||
'attachment',
|
||||
'link',
|
||||
'filename',
|
||||
'comment',
|
||||
'upload_date',
|
||||
@@ -393,7 +395,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SalesOrderSerializer(InvenTreeModelSerializer):
|
||||
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrder object
|
||||
"""
|
||||
@@ -594,8 +596,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
Serializers for the SalesOrderAttachment model
|
||||
"""
|
||||
|
||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = SalesOrderAttachment
|
||||
|
||||
@@ -604,6 +604,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'order',
|
||||
'attachment',
|
||||
'filename',
|
||||
'link',
|
||||
'comment',
|
||||
'upload_date',
|
||||
]
|
||||
|
||||
@@ -53,15 +53,17 @@
|
||||
<span class='fas fa-shopping-cart icon-blue'></span>
|
||||
</button>
|
||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='receive-order' title='{% trans "Receive items" %}'>
|
||||
<span class='fas fa-sign-in-alt icon-blue'></span>
|
||||
<button type='button' class='btn btn-primary' id='receive-order' title='{% trans "Receive items" %}'>
|
||||
<span class='fas fa-sign-in-alt'></span>
|
||||
{% trans "Receive Items" %}
|
||||
</button>
|
||||
<button type='button' class='btn btn-outline-secondary' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
||||
<span class='fas fa-check-circle icon-green'></span>
|
||||
<button type='button' class='btn btn-success' id='complete-order' title='{% trans "Mark order as complete" %}'>
|
||||
<span class='fas fa-check-circle'></span>
|
||||
{% trans "Complete Order" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock actions %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class='part-thumb'
|
||||
@@ -75,24 +77,18 @@ src="{% static 'img/blank_image.png' %}"
|
||||
|
||||
{% block details %}
|
||||
|
||||
<h4>
|
||||
{% 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'>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Order Reference" %}</td>
|
||||
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
|
||||
</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>
|
||||
@@ -103,6 +99,14 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block details_right %}
|
||||
<table class='table table-condensed table-striped'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
{% block sidebar %}
|
||||
{% 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 %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
|
||||
{% include "sidebar_item.html" with label='received-items' text="Received Stock" icon="fa-sign-in-alt" %}
|
||||
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
|
||||
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
|
||||
{% trans "Line Items" as text %}
|
||||
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
|
||||
{% trans "Received Stock" as text %}
|
||||
{% 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" %}
|
||||
</button>
|
||||
{% 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" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -124,51 +124,16 @@
|
||||
}
|
||||
);
|
||||
|
||||
loadAttachmentTable(
|
||||
'{% url "api-po-attachment-list" %}',
|
||||
{
|
||||
loadAttachmentTable('{% url "api-po-attachment-list" %}', {
|
||||
filters: {
|
||||
order: {{ order.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/order/po/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
filename: {},
|
||||
comment: {},
|
||||
},
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Edit Attachment" %}',
|
||||
});
|
||||
},
|
||||
onDelete: function(pk) {
|
||||
|
||||
constructForm(`/api/order/po/attachment/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
||||
title: '{% trans "Delete Attachment" %}',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$("#new-attachment").click(function() {
|
||||
|
||||
constructForm('{% url "api-po-attachment-list" %}', {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
attachment: {},
|
||||
comment: {},
|
||||
order: {
|
||||
value: {{ order.pk }},
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
reload: true,
|
||||
title: '{% trans "Add Attachment" %}',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loadStockTable($("#stock-table"), {
|
||||
|
||||
@@ -68,17 +68,33 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock actions %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<h4>
|
||||
{% sales_order_status_label order.status large=True %}
|
||||
<table class='table table-striped table-condensed'>
|
||||
<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 %}
|
||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<p>{{ order.description }}{% include "clip.html"%}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class='info-messages'>
|
||||
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
|
||||
@@ -93,21 +109,6 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% block details_right %}
|
||||
<table class='table table-striped table-condensed'>
|
||||
<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 %}
|
||||
<tr>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
|
||||
@@ -110,55 +110,21 @@
|
||||
},
|
||||
label: 'attachment',
|
||||
success: function(data, status, xhr) {
|
||||
location.reload();
|
||||
reloadAttachmentTable();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
loadAttachmentTable(
|
||||
'{% url "api-so-attachment-list" %}',
|
||||
{
|
||||
loadAttachmentTable('{% url "api-so-attachment-list" %}', {
|
||||
filters: {
|
||||
order: {{ order.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/order/so/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
filename: {},
|
||||
comment: {},
|
||||
},
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Edit Attachment" %}',
|
||||
});
|
||||
},
|
||||
onDelete: function(pk) {
|
||||
constructForm(`/api/order/so/attachment/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
||||
title: '{% trans "Delete Attachment" %}',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$("#new-attachment").click(function() {
|
||||
|
||||
constructForm('{% url "api-so-attachment-list" %}', {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
attachment: {},
|
||||
comment: {},
|
||||
order: {
|
||||
value: {{ order.pk }},
|
||||
hidden: true
|
||||
}
|
||||
hidden: true,
|
||||
},
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Add Attachment" %}'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loadBuildTable($("#builds-table"), {
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
|
||||
{% include "sidebar_item.html" with label='order-builds' text="Build Orders" icon="fa-tools" %}
|
||||
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
|
||||
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
|
||||
{% trans "Line Items" as text %}
|
||||
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
|
||||
{% trans "Build Orders" as text %}
|
||||
{% 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['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):
|
||||
|
||||
url = reverse('api-po-attachment-list')
|
||||
|
||||
@@ -205,7 +205,7 @@ class BomItemResource(ModelResource):
|
||||
|
||||
# If we are not generating an "import" template,
|
||||
# just return the complete list of fields
|
||||
if not self.is_importing:
|
||||
if not getattr(self, 'is_importing', False):
|
||||
return fields
|
||||
|
||||
# Otherwise, remove some fields we are not interested in
|
||||
|
||||
+140
-21
@@ -26,7 +26,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from .models import Part, PartCategory
|
||||
from .models import Part, PartCategory, PartRelated
|
||||
from .models import BomItem, BomItemSubstitute
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
from .models import PartAttachment, PartTestTemplate
|
||||
@@ -42,7 +42,7 @@ from build.models import Build
|
||||
|
||||
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.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):
|
||||
""" API endpoint for detail view of a single Part object """
|
||||
|
||||
@@ -901,6 +928,40 @@ class PartList(generics.ListCreateAPIView):
|
||||
|
||||
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?
|
||||
starred = params.get('starred', None)
|
||||
|
||||
@@ -1014,9 +1075,48 @@ class PartList(generics.ListCreateAPIView):
|
||||
'revision',
|
||||
'keywords',
|
||||
'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):
|
||||
""" 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')
|
||||
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'
|
||||
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')
|
||||
@@ -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_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):
|
||||
"""
|
||||
@@ -1435,6 +1541,12 @@ part_api_urls = [
|
||||
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
|
||||
url(r'^parameter/', include([
|
||||
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+)/', 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'),
|
||||
]
|
||||
|
||||
@@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
||||
|
||||
uids = []
|
||||
|
||||
def add_items(items, level, cascade):
|
||||
def add_items(items, level, cascade=True):
|
||||
# Add items at a given layer
|
||||
for item in items:
|
||||
|
||||
|
||||
+1
-15
@@ -17,7 +17,7 @@ from InvenTree.fields import RoundingDecimalFormField
|
||||
import common.models
|
||||
from common.forms import MatchItemForm
|
||||
|
||||
from .models import Part, PartCategory, PartRelated
|
||||
from .models import Part, PartCategory
|
||||
from .models import PartParameterTemplate
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||
@@ -157,20 +157,6 @@ class BomMatchItemForm(MatchItemForm):
|
||||
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):
|
||||
""" Form for setting the category of multiple Part objects """
|
||||
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
||||
from stock.models import StockItem
|
||||
|
||||
from .models import (BomItem, BomItemSubstitute,
|
||||
Part, PartAttachment, PartCategory,
|
||||
Part, PartAttachment, PartCategory, PartRelated,
|
||||
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
||||
PartStar, PartTestTemplate, PartCategoryParameterTemplate,
|
||||
PartInternalPriceBreak)
|
||||
@@ -75,8 +75,6 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
Serializer for the PartAttachment class
|
||||
"""
|
||||
|
||||
attachment = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = PartAttachment
|
||||
|
||||
@@ -85,6 +83,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
'part',
|
||||
'attachment',
|
||||
'filename',
|
||||
'link',
|
||||
'comment',
|
||||
'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):
|
||||
""" Serializer for a PartStar object """
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
{% block sidebar %}
|
||||
{% 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 %}
|
||||
|
||||
{% block heading %}
|
||||
|
||||
@@ -61,29 +61,43 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block details_left %}
|
||||
<table class='table table-striped table-condensed'>
|
||||
<col width='25'>
|
||||
{% if category %}
|
||||
<p>{{ category.description }}</p>
|
||||
{% else %}
|
||||
<p>{% trans "Top level part category" %}</p>
|
||||
{% if category.description %}
|
||||
<tr>
|
||||
<td><span class='fas fa-info-circle'></span></td>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ category.description }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
<tr>
|
||||
<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 %}
|
||||
|
||||
{% if category %}
|
||||
<table class='table table-condensed table-striped'>
|
||||
<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 %}
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
{% 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>
|
||||
<td><span class='fas fa-sitemap'></span></td>
|
||||
<td>{% trans "Subcategories" %}</td>
|
||||
@@ -124,7 +131,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock details_right %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
|
||||
{% settings_value 'PART_SHOW_IMPORT' as show_import %}
|
||||
|
||||
{% include "sidebar_item.html" with label="subcategories" text="Subcategories" icon="fa-sitemap" %}
|
||||
{% include "sidebar_item.html" with label="parts" text="Parts" icon="fa-shapes" %}
|
||||
{% trans "Subcategories" as text %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
@@ -11,113 +11,6 @@
|
||||
|
||||
{% 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-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
@@ -330,33 +223,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id='table-related-part' class='table table-condensed table-striped' data-toolbar='#related-button-toolbar'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field='part' data-serachable='true'>{% trans "Part" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in part.get_related_parts %}
|
||||
{% with part_related=item.0 part=item.1 %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class='hover-icon'>
|
||||
<img class='hover-img-thumb' src='{{ part.get_thumbnail_url }}'>
|
||||
<img class='hover-img-large' src='{{ part.get_thumbnail_url }}'>
|
||||
</a>
|
||||
<a href='/part/{{ part.id }}/'>{{ part }}</a>
|
||||
<div class='btn-group' style='float: right;'>
|
||||
{% if roles.part.change %}
|
||||
<button title='{% trans "Delete" %}' class='btn btn-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>
|
||||
<table id='related-parts-table' class='table table-striped table-condensed' data-toolbar='#related-button-toolbar'></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -771,15 +638,34 @@
|
||||
|
||||
// Load the "related parts" tab
|
||||
onPanelLoad("related-parts", function() {
|
||||
$('#table-related-part').inventreeTable({
|
||||
});
|
||||
|
||||
loadRelatedPartsTable(
|
||||
"#related-parts-table",
|
||||
{{ part.pk }}
|
||||
);
|
||||
|
||||
$("#add-related-part").click(function() {
|
||||
launchModalForm("{% url 'part-related-create' %}", {
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
|
||||
constructForm('{% url "api-part-related-list" %}', {
|
||||
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() {
|
||||
loadAttachmentTable(
|
||||
'{% url "api-part-attachment-list" %}',
|
||||
{
|
||||
loadAttachmentTable('{% url "api-part-attachment-list" %}', {
|
||||
filters: {
|
||||
part: {{ part.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/part/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
filename: {},
|
||||
comment: {},
|
||||
},
|
||||
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,
|
||||
});
|
||||
part: {
|
||||
value: {{ part.pk }},
|
||||
hidden: true
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
enableDragAndDrop(
|
||||
'#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 %}
|
||||
{% 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 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -99,11 +99,14 @@
|
||||
|
||||
{% block details %}
|
||||
|
||||
|
||||
</h4>
|
||||
<!-- Properties -->
|
||||
<h4>
|
||||
<div id='part-properties' class='btn-group'>
|
||||
<table class='table table-striped table-condensed' id='part-info-table'>
|
||||
<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 %}
|
||||
 
|
||||
<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' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</h5>
|
||||
</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 -->
|
||||
<div class='info-messages'>
|
||||
@@ -157,7 +175,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock details %}
|
||||
|
||||
{% block details_right %}
|
||||
<table class='table table-condensed table-striped'>
|
||||
@@ -231,7 +249,118 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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.super }}
|
||||
@@ -439,4 +568,24 @@
|
||||
});
|
||||
{% 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 %}
|
||||
@@ -5,34 +5,47 @@
|
||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||
{% settings_value 'PART_SHOW_RELATED' as show_related %}
|
||||
|
||||
{% include "sidebar_item.html" with label="part-details" text="Details" icon="fa-shapes" %}
|
||||
{% include "sidebar_item.html" with label="part-parameters" text="Parameters" icon="fa-th-list" %}
|
||||
{% trans "Parameters" as text %}
|
||||
{% include "sidebar_item.html" with label="part-parameters" text=text icon="fa-th-list" %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% include "sidebar_item.html" with label="suppliers" text="Suppliers" icon="fa-building" %}
|
||||
{% include "sidebar_item.html" with label="purchase-orders" text="Purchase Orders" icon="fa-shopping-cart" %}
|
||||
{% trans "Suppliers" as text %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% include "sidebar_item.html" with label="part-attachments" text="Attachments" icon="fa-paperclip" %}
|
||||
{% include "sidebar_item.html" with label="part-notes" text="Notes" icon="fa-clipboard" %}
|
||||
{% trans "Attachments" as text %}
|
||||
{% 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
|
||||
)
|
||||
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from .models import Part, PartRelated
|
||||
from .models import Part
|
||||
|
||||
|
||||
class PartViewTestCase(TestCase):
|
||||
@@ -145,36 +145,6 @@ class PartDetailTest(PartViewTestCase):
|
||||
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):
|
||||
""" Tests for the Part QR Code AJAX view """
|
||||
|
||||
|
||||
@@ -12,10 +12,6 @@ from django.conf.urls import url, include
|
||||
|
||||
from . import views
|
||||
|
||||
part_related_urls = [
|
||||
url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'),
|
||||
url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'),
|
||||
]
|
||||
|
||||
sale_price_break_urls = [
|
||||
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
||||
@@ -96,9 +92,6 @@ part_urls = [
|
||||
# Part category
|
||||
url(r'^category/', include(category_urls)),
|
||||
|
||||
# Part related
|
||||
url(r'^related-parts/', include(part_related_urls)),
|
||||
|
||||
# Part price breaks
|
||||
url(r'^sale-price/', include(sale_price_break_urls)),
|
||||
|
||||
|
||||
+7
-72
@@ -30,7 +30,7 @@ import io
|
||||
from rapidfuzz import fuzz
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from .models import PartCategory, Part, PartRelated
|
||||
from .models import PartCategory, Part
|
||||
from .models import PartParameterTemplate
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import BomItem
|
||||
@@ -85,75 +85,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
|
||||
return context
|
||||
|
||||
|
||||
class PartRelatedCreate(AjaxCreateView):
|
||||
""" View for creating a new PartRelated object
|
||||
|
||||
- The view only makes sense if a Part object is passed to it
|
||||
"""
|
||||
model = PartRelated
|
||||
form_class = part_forms.CreatePartRelatedForm
|
||||
ajax_form_title = _("Add Related Part")
|
||||
ajax_template_name = "modal_form.html"
|
||||
|
||||
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):
|
||||
""" 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
|
||||
|
||||
# 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')
|
||||
else:
|
||||
elif stock_item.tracking_info.count() > 0:
|
||||
line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
|
||||
else:
|
||||
# Not enough information
|
||||
continue
|
||||
|
||||
price_history.append(line)
|
||||
|
||||
ctx['price_history'] = price_history
|
||||
|
||||
+13
-1
@@ -313,7 +313,7 @@ class StockFilter(rest_filters.FilterSet):
|
||||
# Serial number filtering
|
||||
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 = 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')
|
||||
|
||||
@@ -703,6 +703,18 @@ class StockList(generics.ListCreateAPIView):
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
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?
|
||||
allocated = params.get('allocated', None)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError, FieldError
|
||||
@@ -39,6 +38,7 @@ import label.models
|
||||
from InvenTree.status_codes import StockStatus, StockHistoryCode
|
||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
||||
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
||||
from InvenTree.serializers import extract_int
|
||||
|
||||
from users.models import Owner
|
||||
|
||||
@@ -236,17 +236,7 @@ class StockItem(MPTTModel):
|
||||
serial_int = 0
|
||||
|
||||
if serial is not None:
|
||||
|
||||
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
|
||||
serial_int = extract_int(str(serial))
|
||||
|
||||
self.serial_int = serial_int
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from company.serializers import SupplierPartSerializer
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.serializers
|
||||
from InvenTree.serializers import InvenTreeDecimalField
|
||||
from InvenTree.serializers import InvenTreeDecimalField, extract_int
|
||||
|
||||
from part.serializers import PartBriefSerializer
|
||||
|
||||
@@ -73,6 +73,11 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'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):
|
||||
""" Serializer for a StockItem:
|
||||
@@ -420,8 +425,6 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
|
||||
|
||||
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!
|
||||
|
||||
class Meta:
|
||||
@@ -432,6 +435,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
|
||||
'stock_item',
|
||||
'attachment',
|
||||
'filename',
|
||||
'link',
|
||||
'comment',
|
||||
'upload_date',
|
||||
'user',
|
||||
|
||||
@@ -221,55 +221,16 @@
|
||||
}
|
||||
);
|
||||
|
||||
loadAttachmentTable(
|
||||
'{% url "api-stock-attachment-list" %}',
|
||||
{
|
||||
loadAttachmentTable('{% url "api-stock-attachment-list" %}', {
|
||||
filters: {
|
||||
stock_item: {{ item.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/stock/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
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: {
|
||||
value: {{ item.pk }},
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
reload: true,
|
||||
title: '{% trans "Add Attachment" %}',
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
loadStockTestResultsTable(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Stock Item" %}: {{ item.part.full_name}}
|
||||
{% endblock %}
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
|
||||
@@ -100,7 +100,9 @@
|
||||
<!-- Edit stock item -->
|
||||
{% if roles.stock.change and not item.is_building %}
|
||||
<div class='btn-group'>
|
||||
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-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'>
|
||||
{% 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>
|
||||
@@ -118,38 +120,101 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock actions %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<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 %}
|
||||
|
||||
<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 %}
|
||||
{% if owner_control.value == "True" %}
|
||||
{% authorized_owners item.owner as owners %}
|
||||
{% 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'>
|
||||
|
||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||
@@ -214,49 +279,12 @@
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock details %}
|
||||
|
||||
{% block details_right %}
|
||||
<table class="table table-striped">
|
||||
<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>
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><span class='fas fa-vial'></span></td>
|
||||
@@ -433,6 +428,7 @@
|
||||
$("#stock-serialize").click(function() {
|
||||
|
||||
serializeStockItem({{ item.pk }}, {
|
||||
part: {{ item.part.pk }},
|
||||
reload: true,
|
||||
data: {
|
||||
quantity: {{ item.quantity }},
|
||||
@@ -603,4 +599,8 @@ $("#stock-return-from-customer").click(function() {
|
||||
|
||||
{% endif %}
|
||||
|
||||
$('#serial-number-search').click(function() {
|
||||
findStockItemBySerialNumber({{ item.part.pk }});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user