2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00
This commit is contained in:
Matthias 2021-12-02 17:24:07 +01:00
commit 0f0460f8ea
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
162 changed files with 65207 additions and 59759 deletions

View File

@ -0,0 +1,37 @@
name: Check Translations
on:
push:
branches:
- l10
pull_request:
branches:
- l10
jobs:
check:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INVENTREE_DB_NAME: './test_db.sqlite'
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
INVENTREE_DEBUG: info
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install gettext
pip3 install invoke
invoke install
- name: Test Translations
run: invoke translate
- name: Check Migration Files
run: python3 ci/check_migration_files.py

View File

@ -23,11 +23,13 @@ jobs:
INVENTREE_MEDIA_ROOT: ./media INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static INVENTREE_STATIC_ROOT: ./static
steps: steps:
- name: Install node.js
uses: actions/setup-node@v2
- run: npm install
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
@ -41,7 +43,6 @@ jobs:
invoke static invoke static
- name: Check HTML Files - name: Check HTML Files
run: | run: |
npm install markuplint
npx markuplint InvenTree/build/templates/build/*.html npx markuplint InvenTree/build/templates/build/*.html
npx markuplint InvenTree/company/templates/company/*.html npx markuplint InvenTree/company/templates/company/*.html
npx markuplint InvenTree/order/templates/order/*.html npx markuplint InvenTree/order/templates/order/*.html

View File

@ -23,11 +23,13 @@ jobs:
INVENTREE_MEDIA_ROOT: ./media INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static INVENTREE_STATIC_ROOT: ./static
steps: steps:
- name: Install node.js
uses: actions/setup-node@v2
- run: npm install
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
@ -45,6 +47,5 @@ jobs:
python check_js_templates.py python check_js_templates.py
- name: Lint Javascript Files - name: Lint Javascript Files
run: | run: |
npm install eslint eslint-config-google
invoke render-js-files invoke render-js-files
npx eslint js_tmp/*.js npx eslint js_tmp/*.js

1
.gitignore vendored
View File

@ -78,5 +78,4 @@ locale_stats.json
# node.js # node.js
package-lock.json package-lock.json
package.json
node_modules/ node_modules/

View File

@ -2,8 +2,7 @@
Pull rendered copies of the templated Pull rendered copies of the templated
""" """
from django.http import response from django.test import TestCase
from django.test import TestCase, testcases
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
import os import os

View File

@ -21,7 +21,8 @@ from django.dispatch import receiver
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from mptt.exceptions import InvalidMove from mptt.exceptions import InvalidMove
from .validators import validate_tree_name from InvenTree.fields import InvenTreeURLField
from InvenTree.validators import validate_tree_name
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
@ -48,6 +49,9 @@ class ReferenceIndexingMixin(models.Model):
""" """
A mixin for keeping track of numerical copies of the "reference" field. A mixin for keeping track of numerical copies of the "reference" field.
!!DANGER!! always add `ReferenceIndexingSerializerMixin`to all your models serializers to
ensure the reference field is not too big
Here, we attempt to convert a "reference" field value (char) to an integer, Here, we attempt to convert a "reference" field value (char) to an integer,
for performing fast natural sorting. for performing fast natural sorting.
@ -68,6 +72,12 @@ class ReferenceIndexingMixin(models.Model):
reference = getattr(self, 'reference', '') reference = getattr(self, 'reference', '')
self.reference_int = extract_int(reference)
reference_int = models.BigIntegerField(default=0)
def extract_int(reference):
# Default value if we cannot convert to an integer # Default value if we cannot convert to an integer
ref_int = 0 ref_int = 0
@ -80,21 +90,21 @@ class ReferenceIndexingMixin(models.Model):
ref_int = int(ref) ref_int = int(ref)
except: except:
ref_int = 0 ref_int = 0
return ref_int
self.reference_int = ref_int
reference_int = models.IntegerField(default=0)
class InvenTreeAttachment(models.Model): class InvenTreeAttachment(models.Model):
""" Provides an abstracted class for managing file attachments. """ Provides an abstracted class for managing file attachments.
An attachment can be either an uploaded file, or an external URL
Attributes: Attributes:
attachment: File attachment: File
comment: String descriptor for the attachment comment: String descriptor for the attachment
user: User associated with file upload user: User associated with file upload
upload_date: Date the file was uploaded upload_date: Date the file was uploaded
""" """
def getSubdir(self): def getSubdir(self):
""" """
Return the subdirectory under which attachments should be stored. Return the subdirectory under which attachments should be stored.
@ -103,11 +113,32 @@ class InvenTreeAttachment(models.Model):
return "attachments" return "attachments"
def save(self, *args, **kwargs):
# Either 'attachment' or 'link' must be specified!
if not self.attachment and not self.link:
raise ValidationError({
'attachment': _('Missing file'),
'link': _('Missing external link'),
})
super().save(*args, **kwargs)
def __str__(self): def __str__(self):
if self.attachment is not None:
return os.path.basename(self.attachment.name) return os.path.basename(self.attachment.name)
else:
return str(self.link)
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'), attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
help_text=_('Select file to attach')) help_text=_('Select file to attach'),
blank=True, null=True
)
link = InvenTreeURLField(
blank=True, null=True,
verbose_name=_('Link'),
help_text=_('Link to external URL')
)
comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment')) comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment'))
@ -123,7 +154,10 @@ class InvenTreeAttachment(models.Model):
@property @property
def basename(self): def basename(self):
if self.attachment:
return os.path.basename(self.attachment.name) return os.path.basename(self.attachment.name)
else:
return None
@basename.setter @basename.setter
def basename(self, fn): def basename(self, fn):

View File

@ -16,6 +16,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import models
from djmoney.contrib.django_rest_framework.fields import MoneyField from djmoney.contrib.django_rest_framework.fields import MoneyField
from djmoney.money import Money from djmoney.money import Money
@ -27,6 +28,8 @@ from rest_framework.fields import empty
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.serializers import DecimalField from rest_framework.serializers import DecimalField
from .models import extract_int
class InvenTreeMoneySerializer(MoneyField): class InvenTreeMoneySerializer(MoneyField):
""" """
@ -239,20 +242,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return data return data
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer): class ReferenceIndexingSerializerMixin():
""" """
Special case of an InvenTreeModelSerializer, which handles an "attachment" model. This serializer mixin ensures the the reference is not to big / small
for the BigIntegerField
The only real addition here is that we support "renaming" of the attachment file.
""" """
def validate_reference(self, value):
# The 'filename' field must be present in the serializer if extract_int(value) > models.BigIntegerField.MAX_BIGINT:
filename = serializers.CharField( raise serializers.ValidationError('reference is to to big')
label=_('Filename'), return value
required=False,
source='basename',
allow_blank=False,
)
class InvenTreeAttachmentSerializerField(serializers.FileField): class InvenTreeAttachmentSerializerField(serializers.FileField):
@ -284,6 +282,27 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
return os.path.join(str(settings.MEDIA_URL), str(value)) return os.path.join(str(settings.MEDIA_URL), str(value))
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
"""
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
The only real addition here is that we support "renaming" of the attachment file.
"""
attachment = InvenTreeAttachmentSerializerField(
required=False,
allow_null=False,
)
# The 'filename' field must be present in the serializer
filename = serializers.CharField(
label=_('Filename'),
required=False,
source='basename',
allow_blank=False,
)
class InvenTreeImageSerializerField(serializers.ImageField): class InvenTreeImageSerializerField(serializers.ImageField):
""" """
Custom image serializer. Custom image serializer.

View File

@ -26,6 +26,7 @@ import moneyed
import yaml import yaml
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
import django.conf.locale
def _is_true(x): def _is_true(x):
@ -256,7 +257,7 @@ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'user_sessions', # db user sessions
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.sites', 'django.contrib.sites',
@ -304,7 +305,7 @@ INSTALLED_APPS = [
MIDDLEWARE = CONFIG.get('middleware', [ MIDDLEWARE = CONFIG.get('middleware', [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'user_sessions.middleware.SessionMiddleware', # db user sessions
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@ -634,6 +635,12 @@ if _cache_host:
# as well # as well
Q_CLUSTER["django_redis"] = "worker" Q_CLUSTER["django_redis"] = "worker"
# database user sessions
SESSION_ENGINE = 'user_sessions.backends.db'
LOGOUT_REDIRECT_URL = 'index'
SILENCED_SYSTEM_CHECKS = [
'admin.E410',
]
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
@ -673,7 +680,7 @@ LANGUAGES = [
('el', _('Greek')), ('el', _('Greek')),
('en', _('English')), ('en', _('English')),
('es', _('Spanish')), ('es', _('Spanish')),
('es-mx', _('Spanish (Mexican')), ('es-mx', _('Spanish (Mexican)')),
('fr', _('French')), ('fr', _('French')),
('he', _('Hebrew')), ('he', _('Hebrew')),
('it', _('Italian')), ('it', _('Italian')),
@ -691,6 +698,25 @@ LANGUAGES = [
('zh-cn', _('Chinese')), ('zh-cn', _('Chinese')),
] ]
# Testing interface translations
if get_setting('TEST_TRANSLATIONS', False):
# Set default language
LANGUAGE_CODE = 'xx'
# Add to language catalog
LANGUAGES.append(('xx', 'Test'))
# Add custom languages not provided by Django
EXTRA_LANG_INFO = {
'xx': {
'code': 'xx',
'name': 'Test',
'name_local': 'Test'
},
}
LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
django.conf.locale.LANG_INFO = LANG_INFO
# Currencies available for use # Currencies available for use
CURRENCIES = CONFIG.get( CURRENCIES = CONFIG.get(
'currencies', 'currencies',

View File

@ -781,6 +781,7 @@ input[type="submit"] {
.btn-small { .btn-small {
padding: 3px; padding: 3px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px;
} }
.btn-remove { .btn-remove {

View File

@ -38,6 +38,7 @@ from rest_framework.documentation import include_docs_urls
from .views import auth_request from .views import auth_request
from .views import IndexView, SearchView, DatabaseStatsView from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView
from .views import CurrencyRefreshView from .views import CurrencyRefreshView
from .views import AppearanceSelectView, SettingCategorySelectView from .views import AppearanceSelectView, SettingCategorySelectView
from .views import DynamicJsView from .views import DynamicJsView
@ -157,6 +158,10 @@ frontendpatterns = [
url(r'^search/', SearchView.as_view(), name='search'), url(r'^search/', SearchView.as_view(), name='search'),
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
# DB user sessions
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
url(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ),
# Single Sign On / allauth # Single Sign On / allauth
# overrides of urlpatterns # overrides of urlpatterns
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'), url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),

View File

@ -12,11 +12,15 @@ import common.models
INVENTREE_SW_VERSION = "0.6.0 dev" INVENTREE_SW_VERSION = "0.6.0 dev"
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 18 INVENTREE_API_VERSION = 19
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v19 -> 2021-12-02
- Adds the ability to filter the StockItem API by "part_tree"
- Returns only stock items which match a particular part.tree_id field
v18 -> 2021-11-15 v18 -> 2021-11-15
- Adds the ability to filter BomItem API by "uses" field - Adds the ability to filter BomItem API by "uses" field
- This returns a list of all BomItems which "use" the specified part - This returns a list of all BomItems which "use" the specified part

View File

@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.timezone import now
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf import settings from django.conf import settings
@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from allauth.account.views import EmailView, PasswordResetFromKeyView from allauth.account.views import EmailView, PasswordResetFromKeyView
from allauth.socialaccount.views import ConnectionsView from allauth.socialaccount.views import ConnectionsView
from user_sessions.views import SessionDeleteView, SessionDeleteOtherView
from common.settings import currency_code_default, currency_codes from common.settings import currency_code_default, currency_codes
@ -733,6 +735,10 @@ class SettingsView(TemplateView):
ctx["request"] = self.request ctx["request"] = self.request
ctx['social_form'] = DisconnectForm(request=self.request) ctx['social_form'] = DisconnectForm(request=self.request)
# user db sessions
ctx['session_key'] = self.request.session.session_key
ctx['session_list'] = self.request.user.session_set.filter(expire_date__gt=now()).order_by('-last_activity')
return ctx return ctx
@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView):
success_url = reverse_lazy("account_login") success_url = reverse_lazy("account_login")
class UserSessionOverride():
"""overrides sucessurl to lead to settings"""
def get_success_url(self):
return str(reverse_lazy('settings'))
class CustomSessionDeleteView(UserSessionOverride, SessionDeleteView):
pass
class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView):
pass
class CurrencyRefreshView(RedirectView): class CurrencyRefreshView(RedirectView):
""" """
POST endpoint to refresh / update exchange rates POST endpoint to refresh / update exchange rates

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0032_auto_20211014_0632'),
]
operations = [
migrations.AddField(
model_name='buildorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='buildorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-12-01 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0033_auto_20211128_0151'),
]
operations = [
migrations.AlterField(
model_name='build',
name='reference_int',
field=models.BigIntegerField(default=0),
),
]

View File

@ -16,7 +16,7 @@ from rest_framework import serializers
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
import InvenTree.helpers import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField
@ -32,7 +32,7 @@ from users.serializers import OwnerSerializer
from .models import Build, BuildItem, BuildOrderAttachment from .models import Build, BuildItem, BuildOrderAttachment
class BuildSerializer(InvenTreeModelSerializer): class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" """
Serializes a Build object Serializes a Build object
""" """
@ -516,8 +516,6 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializer for a BuildAttachment Serializer for a BuildAttachment
""" """
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta: class Meta:
model = BuildOrderAttachment model = BuildOrderAttachment
@ -525,6 +523,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
'pk', 'pk',
'build', 'build',
'attachment', 'attachment',
'link',
'filename', 'filename',
'comment', 'comment',
'upload_date', 'upload_date',

View File

@ -12,7 +12,7 @@
{% block breadcrumbs %} {% block breadcrumbs %}
<li class='breadcrumb-item'><a href='{% url "build-index" %}'>{% trans "Build Orders" %}</a></li> <li class='breadcrumb-item'><a href='{% url "build-index" %}'>{% trans "Build Orders" %}</a></li>
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "build-detail" build.id %}'>{{ build }}</a></li> <li class="breadcrumb-item active" aria-current="page"><a href='{% url "build-detail" build.id %}'>{{ build }}</a></li>
{% endblock %} {% endblock breadcrumbs %}
{% block thumbnail %} {% block thumbnail %}
<img class="part-thumb" <img class="part-thumb"
@ -21,7 +21,7 @@ src="{{ build.part.image.url }}"
{% else %} {% else %}
src="{% static 'img/blank_image.png' %}" src="{% static 'img/blank_image.png' %}"
{% endif %}/> {% endif %}/>
{% endblock %} {% endblock thumbnail %}
{% block heading %} {% block heading %}
{% trans "Build Order" %} {{ build }} {% trans "Build Order" %} {{ build }}
@ -66,11 +66,23 @@ src="{% static 'img/blank_image.png' %}"
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock actions %}
{% block details %} {% block details %}
<p>{{ build.title }}</p> <table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Part" %}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Build Description" %}</td>
<td>{{ build.title }}</td>
</tr>
</table>
<div class='info-messages'> <div class='info-messages'>
{% if build.sales_order %} {% if build.sales_order %}
@ -114,11 +126,7 @@ src="{% static 'img/blank_image.png' %}"
{% block details_right %} {% block details_right %}
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tr> <col width='25'>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Part" %}</td>
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
</tr>
<tr> <tr>
<td></td> <td></td>
<td>{% trans "Quantity" %}</td> <td>{% trans "Quantity" %}</td>

View File

@ -431,54 +431,18 @@ enableDragAndDrop(
} }
); );
// Callback for creating a new attachment loadAttachmentTable('{% url "api-build-attachment-list" %}', {
$('#new-attachment').click(function() { filters: {
build: {{ build.pk }},
constructForm('{% url "api-build-attachment-list" %}', { },
fields: { fields: {
attachment: {},
comment: {},
build: { build: {
value: {{ build.pk }}, value: {{ build.pk }},
hidden: true, hidden: true,
} }
}, }
method: 'POST',
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}',
});
}); });
loadAttachmentTable(
'{% url "api-build-attachment-list" %}',
{
filters: {
build: {{ build.pk }},
},
onEdit: function(pk) {
var url = `/api/build/attachment/${pk}/`;
constructForm(url, {
fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/build/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$('#edit-notes').click(function() { $('#edit-notes').click(function() {
constructForm('{% url "api-build-detail" build.pk %}', { constructForm('{% url "api-build-detail" build.pk %}', {
fields: { fields: {

View File

@ -2,14 +2,21 @@
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% include "sidebar_item.html" with label='details' text="Build Order Details" icon="fa-info-circle" %} {% trans "Build Order Details" as text %}
{% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %}
{% if build.active %} {% if build.active %}
{% include "sidebar_item.html" with label='allocate' text="Allocate Stock" icon="fa-tasks" %} {% trans "Allocate Stock" as text %}
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
{% endif %} {% endif %}
{% if not build.is_complete %} {% if not build.is_complete %}
{% include "sidebar_item.html" with label='outputs' text="Pending Items" icon="fa-tools" %} {% trans "Pending Items" as text %}
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
{% endif %} {% endif %}
{% include "sidebar_item.html" with label='completed' text="Completed Items" icon="fa-boxes" %} {% trans "Completed Items" as text %}
{% include "sidebar_item.html" with label='children' text="Child Build Orders" icon="fa-sitemap" %} {% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
{% include "sidebar_item.html" with label='attachments' text="Attachments" icon="fa-paperclip" %} {% trans "Child Build Orders" as text %}
{% include "sidebar_item.html" with label='notes' text="Notes" icon="fa-clipboard" %} {% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label='notes' text=text icon="fa-clipboard" %}

View File

@ -108,7 +108,9 @@ class BaseInvenTreeSetting(models.Model):
for key, value in settings.items(): for key, value in settings.items():
validator = cls.get_setting_validator(key) validator = cls.get_setting_validator(key)
if cls.validator_is_bool(validator): if cls.is_protected(key):
value = '***'
elif cls.validator_is_bool(validator):
value = InvenTree.helpers.str2bool(value) value = InvenTree.helpers.str2bool(value)
elif cls.validator_is_int(validator): elif cls.validator_is_int(validator):
try: try:
@ -538,6 +540,19 @@ class BaseInvenTreeSetting(models.Model):
return value return value
@classmethod
def is_protected(cls, key):
"""
Check if the setting value is protected
"""
key = str(key).strip().upper()
if key in cls.GLOBAL_SETTINGS:
return cls.GLOBAL_SETTINGS[key].get('protected', False)
else:
return False
def settings_group_options(): def settings_group_options():
"""build up group tuple for settings based on gour choices""" """build up group tuple for settings based on gour choices"""

View File

@ -45,6 +45,18 @@ class SettingsSerializer(InvenTreeModelSerializer):
return results return results
def get_value(self, obj):
"""
Make sure protected values are not returned
"""
result = obj.value
# never return protected values
if obj.is_protected:
result = '***'
return result
class GlobalSettingsSerializer(SettingsSerializer): class GlobalSettingsSerializer(SettingsSerializer):
""" """

View File

@ -19,21 +19,26 @@
{% include "admin_button.html" with url=url %} {% include "admin_button.html" with url=url %}
{% endif %} {% endif %}
{% if company.is_supplier and roles.purchase_order.add %} {% if company.is_supplier and roles.purchase_order.add %}
<button type='button' class='btn btn-outline-secondary' id='company-order-2' title='{% trans "Create Purchase Order" %}'> <button type='button' class='btn btn-outline-primary' id='company-order-2' title='{% trans "Create Purchase Order" %}'>
<span class='fas fa-shopping-cart'/> <span class='fas fa-shopping-cart'/>
</button> </button>
{% endif %} {% endif %}
{% if perms.company.change_company %} <button id='company-edit-actions' title='{% trans "Company actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<button type='button' class='btn btn-outline-secondary' id='company-edit' title='{% trans "Edit company information" %}'> <span class='fas fa-tools'></span> <span class='caret'></span>
<span class='fas fa-edit icon-green'/>
</button> </button>
{% endif %} <ul class='dropdown-menu' role='menu'>
{% if perms.company.delete_company %} {% if perms.company.change_company %}
<button type='button' class='btn btn-outline-secondary' id='company-delete' title='{% trans "Delete Company" %}'> <li><a class='dropdown-item' href='#' id='company-edit' title='{% trans "Edit company information" %}'>
<span class='fas fa-trash-alt icon-red'/> <span class='fas fa-edit icon-green'></span> {% trans "Edit Company" %}
</button> </a></li>
{% endif %} {% endif %}
{% endblock %} {% if perms.company.delete_company %}
<li><a class='dropdown-item' href='#' id='company-delete' title='{% trans "Delete company" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Company" %}
</a></li>
{% endif %}
</ul>
{% endblock actions %}
{% block thumbnail %} {% block thumbnail %}
<div class='dropzone part-thumb-container' id='company-thumb'> <div class='dropzone part-thumb-container' id='company-thumb'>
@ -56,7 +61,29 @@
{% endblock %} {% endblock %}
{% block details %} {% block details %}
<p>{{ company.description }}</p> <table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Description" %}</td>
<td>{{ company.description }}</td>
</tr>
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{%trans "Manufacturer" %}</td>
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
</tr>
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
</tr>
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
</tr>
</table>
{% endblock %} {% endblock %}
@ -110,22 +137,6 @@
<td>{{ company.contact }}{% include "clip.html"%}</td> <td>{{ company.contact }}{% include "clip.html"%}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{%trans "Manufacturer" %}</td>
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
</tr>
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
</tr>
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
</tr>
</table> </table>
{% endblock %} {% endblock %}

View File

@ -8,7 +8,7 @@ InvenTree | {% trans "Manufacturer Part" %}
{% block sidebar %} {% block sidebar %}
{% include "company/manufacturer_part_sidebar.html" %} {% include "company/manufacturer_part_sidebar.html" %}
{% endblock %} {% endblock sidebar %}
{% block breadcrumbs %} {% block breadcrumbs %}
<li class='breadcrumb-item'><a href='{% url "manufacturer-index" %}'>{% trans "Manufacturers" %}</a></li> <li class='breadcrumb-item'><a href='{% url "manufacturer-index" %}'>{% trans "Manufacturers" %}</a></li>
@ -16,13 +16,13 @@ InvenTree | {% trans "Manufacturer Part" %}
<li class='breadcrumb-item'><a href='{% url "company-detail" part.manufacturer.id %}'>{{ part.manufacturer.name }}</a></li> <li class='breadcrumb-item'><a href='{% url "company-detail" part.manufacturer.id %}'>{{ part.manufacturer.name }}</a></li>
{% endif %} {% endif %}
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "manufacturer-part-detail" part.id %}'>{{ part.MPN }}</a></li> <li class="breadcrumb-item active" aria-current="page"><a href='{% url "manufacturer-part-detail" part.id %}'>{{ part.MPN }}</a></li>
{% endblock %} {% endblock breadcrumbs %}
{% block heading %} {% block heading %}
<h4> <h4>
{% trans "Manufacturer Part" %}: {{ part.part.full_name }} {% trans "Manufacturer Part" %}: {{ part.part.full_name }}
</h4> </h4>
{% endblock %} {% endblock heading %}
{% block actions %} {% block actions %}
{% if user.is_staff and perms.company.change_company %} {% if user.is_staff and perms.company.change_company %}
@ -46,7 +46,7 @@ InvenTree | {% trans "Manufacturer Part" %}
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock actions %}
{% block thumbnail %} {% block thumbnail %}
<img class='part-thumb' <img class='part-thumb'
@ -55,15 +55,11 @@ src='{{ part.part.image.url }}'
{% else %} {% else %}
src="{% static 'img/blank_image.png' %}" src="{% static 'img/blank_image.png' %}"
{% endif %}/> {% endif %}/>
{% endblock %} {% endblock thumbnail %}
{% block details %} {% block details %}
{% endblock %} <table class='table table-striped table-condensed'>
{% block details_right %}
<table class="table table-striped table-condensed">
<col width='25'> <col width='25'>
<tr> <tr>
<td><span class='fas fa-shapes'></span></td> <td><span class='fas fa-shapes'></span></td>
@ -81,6 +77,25 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ part.description }}{% include "clip.html"%}</td> <td>{{ part.description }}{% include "clip.html"%}</td>
</tr> </tr>
{% endif %} {% endif %}
</table>
{% endblock details %}
{% block details_right %}
<table class="table table-striped table-condensed">
<col width='25'>
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "MPN" %}</td>
<td>{{ part.MPN }}{% include "clip.html"%}</td>
</tr>
{% if part.link %} {% if part.link %}
<tr> <tr>
<td><span class='fas fa-link'></span></td> <td><span class='fas fa-link'></span></td>
@ -88,17 +103,8 @@ src="{% static 'img/blank_image.png' %}"
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td> <td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td></tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "MPN" %}</td>
<td>{{ part.MPN }}{% include "clip.html"%}</td>
</tr>
</table> </table>
{% endblock %} {% endblock details_right %}
{% block page_content %} {% block page_content %}

View File

@ -2,5 +2,7 @@
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% include "sidebar_item.html" with label='parameters' text="Parameters" icon="fa-th-list" %} {% trans "Parameters" as text %}
{% include "sidebar_item.html" with label='supplier-parts' text="Supplier Parts" icon="fa-building" %} {% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %}
{% trans "Supplier Parts" as text %}
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}

View File

@ -3,17 +3,24 @@
{% load inventree_extras %} {% load inventree_extras %}
{% if company.is_manufacturer %} {% if company.is_manufacturer %}
{% include "sidebar_item.html" with label='manufacturer-parts' text="Manufactured Parts" icon="fa-industry" %} {% trans "Manufactured Parts" as text %}
{% include "sidebar_item.html" with label='manufacturer-parts' text=text icon="fa-industry" %}
{% endif %} {% endif %}
{% if company.is_supplier %} {% if company.is_supplier %}
{% include "sidebar_item.html" with label='supplier-parts' text="Supplied Parts" icon="fa-building" %} {% trans "Supplied Parts" as text %}
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %} {% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
{% trans "Purchase Orders" as text %}
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
{% endif %} {% endif %}
{% if company.is_manufacturer or company.is_supplier %} {% if company.is_manufacturer or company.is_supplier %}
{% include "sidebar_item.html" with label='company-stock' text="Supplied Stock Items" icon="fa-boxes" %} {% trans "Supplied Stock Items" as text %}
{% include "sidebar_item.html" with label='company-stock' text=text icon="fa-boxes" %}
{% endif %} {% endif %}
{% if company.is_customer %} {% if company.is_customer %}
{% include "sidebar_item.html" with label='sales-orders' text="Sales Orders" icon="fa-truck" %} {% trans "Sales Orders" as text %}
{% include "sidebar_item.html" with label='assigned-stock' text="Assigned Stock Items" icon="fa-sign-out-alt" %} {% include "sidebar_item.html" with label='sales-orders' text=text icon="fa-truck" %}
{% trans "Assigned Stock Items" as text %}
{% include "sidebar_item.html" with label='assigned-stock' text=text icon="fa-sign-out-alt" %}
{% endif %} {% endif %}
{% include "sidebar_item.html" with label='company-notes' text="Notes" icon="fa-clipboard" %} {% trans "Notes" as text %}
{% include "sidebar_item.html" with label='company-notes' text=text icon="fa-clipboard" %}

View File

@ -5,11 +5,11 @@
{% block page_title %} {% block page_title %}
{% inventree_title %} | {% trans "Supplier Part" %} {% inventree_title %} | {% trans "Supplier Part" %}
{% endblock %} {% endblock page_title %}
{% block sidebar %} {% block sidebar %}
{% include "company/supplier_part_sidebar.html" %} {% include "company/supplier_part_sidebar.html" %}
{% endblock %} {% endblock sidebar %}
{% block breadcrumbs %} {% block breadcrumbs %}
<li class='breadcrumb-item'><a href='{% url "supplier-index" %}'>{% trans "Suppliers" %}</a></li> <li class='breadcrumb-item'><a href='{% url "supplier-index" %}'>{% trans "Suppliers" %}</a></li>
@ -17,13 +17,13 @@
<li class='breadcrumb-item'><a href='{% url "company-detail" part.supplier.id %}'>{{ part.supplier.name }}</a></li> <li class='breadcrumb-item'><a href='{% url "company-detail" part.supplier.id %}'>{{ part.supplier.name }}</a></li>
{% endif %} {% endif %}
<li class="breadcrumb-item active" aria-current="page"><a href='{% url "supplier-part-detail" part.id %}'>{{ part.SKU }}</a></li> <li class="breadcrumb-item active" aria-current="page"><a href='{% url "supplier-part-detail" part.id %}'>{{ part.SKU }}</a></li>
{% endblock %} {% endblock breadcrumbs %}
{% block heading %} {% block heading %}
<h4> <h4>
{% trans "Supplier Part" %}: {{ part.SKU }} {% trans "Supplier Part" %}: {{ part.SKU }}
</h4> </h4>
{% endblock %} {% endblock heading %}
{% block actions %} {% block actions %}
{% if user.is_staff and perms.company.change_company %} {% if user.is_staff and perms.company.change_company %}
@ -43,7 +43,7 @@
<span class='fas fa-trash-alt icon-red'/> <span class='fas fa-trash-alt icon-red'/>
</button> </button>
{% endif %} {% endif %}
{% endblock %} {% endblock actions %}
{% block thumbnail %} {% block thumbnail %}
<img class='part-thumb' <img class='part-thumb'
@ -56,15 +56,7 @@ src="{% static 'img/blank_image.png' %}"
{% block details %} {% block details %}
<p> <table class='table table-striped table-condensed'>
{{ part.part.full_name }}
</p>
{% endblock %}
{% block details_right %}
<table class="table table-striped table-condensed">
<col width='25'> <col width='25'>
<tr> <tr>
<td><span class='fas fa-shapes'></span></td> <td><span class='fas fa-shapes'></span></td>
@ -82,13 +74,14 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ part.description }}{% include "clip.html"%}</td> <td>{{ part.description }}{% include "clip.html"%}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if part.link %} </table>
<tr>
<td><span class='fas fa-link'></span></td> {% endblock details %}
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td> {% block details_right %}
</tr>
{% endif %} <table class="table table-striped table-condensed">
<col width='25'>
<tr> <tr>
<td><span class='fas fa-building'></span></td> <td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td> <td>{% trans "Supplier" %}</td>
@ -127,6 +120,13 @@ src="{% static 'img/blank_image.png' %}"
<td>{{ part.note }}{% include "clip.html"%}</td> <td>{{ part.note }}{% include "clip.html"%}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
</table> </table>
{% endblock %} {% endblock %}

View File

@ -2,6 +2,9 @@
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% include "sidebar_item.html" with label='stock' text="Stock Items" icon="fa-boxes" %} {% trans "Stock Items" as text %}
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %} {% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
{% include "sidebar_item.html" with label='pricing' text="Supplier Part Pricing" icon="fa-dollar-sign" %} {% trans "Purchase Orders" as text %}
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
{% trans "Supplier Part Pricing" as text %}
{% include "sidebar_item.html" with label='pricing' text=text icon="fa-dollar-sign" %}

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0052_auto_20211014_0631'),
]
operations = [
migrations.AddField(
model_name='purchaseorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AddField(
model_name='salesorderattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='purchaseorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
migrations.AlterField(
model_name='salesorderattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.5 on 2021-12-01 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0053_auto_20211128_0151'),
]
operations = [
migrations.AlterField(
model_name='purchaseorder',
name='reference_int',
field=models.BigIntegerField(default=0),
),
migrations.AlterField(
model_name='salesorder',
name='reference_int',
field=models.BigIntegerField(default=0),
),
]

View File

@ -17,16 +17,16 @@ from rest_framework.serializers import ValidationError
from sql_util.utils import SubqueryCount from sql_util.utils import SubqueryCount
from common.settings import currency_code_mappings
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField from InvenTree.serializers import ReferenceIndexingSerializerMixin
from InvenTree.status_codes import StockStatus from InvenTree.status_codes import StockStatus
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
import stock.models import stock.models
@ -37,10 +37,10 @@ from .models import PurchaseOrderAttachment, SalesOrderAttachment
from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrder, SalesOrderLineItem
from .models import SalesOrderAllocation from .models import SalesOrderAllocation
from common.settings import currency_code_mappings from users.serializers import OwnerSerializer
class POSerializer(InvenTreeModelSerializer): class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrder object """ """ Serializer for a PurchaseOrder object """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -86,6 +86,8 @@ class POSerializer(InvenTreeModelSerializer):
reference = serializers.CharField(required=True) reference = serializers.CharField(required=True)
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
class Meta: class Meta:
model = PurchaseOrder model = PurchaseOrder
@ -100,6 +102,7 @@ class POSerializer(InvenTreeModelSerializer):
'overdue', 'overdue',
'reference', 'reference',
'responsible', 'responsible',
'responsible_detail',
'supplier', 'supplier',
'supplier_detail', 'supplier_detail',
'supplier_reference', 'supplier_reference',
@ -374,8 +377,6 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializers for the PurchaseOrderAttachment model Serializers for the PurchaseOrderAttachment model
""" """
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta: class Meta:
model = PurchaseOrderAttachment model = PurchaseOrderAttachment
@ -383,6 +384,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
'pk', 'pk',
'order', 'order',
'attachment', 'attachment',
'link',
'filename', 'filename',
'comment', 'comment',
'upload_date', 'upload_date',
@ -393,7 +395,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
] ]
class SalesOrderSerializer(InvenTreeModelSerializer): class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" """
Serializers for the SalesOrder object Serializers for the SalesOrder object
""" """
@ -594,8 +596,6 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializers for the SalesOrderAttachment model Serializers for the SalesOrderAttachment model
""" """
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta: class Meta:
model = SalesOrderAttachment model = SalesOrderAttachment
@ -604,6 +604,7 @@ class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
'order', 'order',
'attachment', 'attachment',
'filename', 'filename',
'link',
'comment', 'comment',
'upload_date', 'upload_date',
] ]

View File

@ -53,15 +53,17 @@
<span class='fas fa-shopping-cart icon-blue'></span> <span class='fas fa-shopping-cart icon-blue'></span>
</button> </button>
{% elif order.status == PurchaseOrderStatus.PLACED %} {% elif order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-outline-secondary' id='receive-order' title='{% trans "Receive items" %}'> <button type='button' class='btn btn-primary' id='receive-order' title='{% trans "Receive items" %}'>
<span class='fas fa-sign-in-alt icon-blue'></span> <span class='fas fa-sign-in-alt'></span>
{% trans "Receive Items" %}
</button> </button>
<button type='button' class='btn btn-outline-secondary' id='complete-order' title='{% trans "Mark order as complete" %}'> <button type='button' class='btn btn-success' id='complete-order' title='{% trans "Mark order as complete" %}'>
<span class='fas fa-check-circle icon-green'></span> <span class='fas fa-check-circle'></span>
{% trans "Complete Order" %}
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock actions %}
{% block thumbnail %} {% block thumbnail %}
<img class='part-thumb' <img class='part-thumb'
@ -75,24 +77,18 @@ src="{% static 'img/blank_image.png' %}"
{% block details %} {% block details %}
<h4> <table class='table table-striped table-condensed'>
{% purchase_order_status_label order.status large=True %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}
</h4>
<p>{{ order.description }}{% include "clip.html"%}</p>
{% endblock %}
{% block details_right %}
<table class='table'>
<col width='25'> <col width='25'>
<tr> <tr>
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td> <td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td> <td>{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
</tr> </tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Order Description" %}</td>
<td>{{ order.description }}{% include "clip.html" %}</td>
</tr>
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td> <td>{% trans "Order Status" %}</td>
@ -103,6 +99,14 @@ src="{% static 'img/blank_image.png' %}"
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
</table>
{% endblock %}
{% block details_right %}
<table class='table table-condensed table-striped'>
<col width='25'>
<tr> <tr>
<td><span class='fas fa-building'></span></td> <td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td> <td>{% trans "Supplier" %}</td>

View File

@ -5,7 +5,8 @@
{% block sidebar %} {% block sidebar %}
{% url "po-detail" order.id as url %} {% url "po-detail" order.id as url %}
{% include "sidebar_item.html" with url=url text="Return to Orders" icon="fa-undo" %} {% trans "Return to Orders" as text %}
{% include "sidebar_item.html" with url=url text=text icon="fa-undo" %}
{% endblock %} {% endblock %}
{% block page_content %} {% block page_content %}

View File

@ -2,7 +2,11 @@
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %} {% trans "Line Items" as text %}
{% include "sidebar_item.html" with label='received-items' text="Received Stock" icon="fa-sign-in-alt" %} {% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %} {% trans "Received Stock" as text %}
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %} {% include "sidebar_item.html" with label='received-items' text=text icon="fa-sign-in-alt" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}

View File

@ -27,7 +27,7 @@
<span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %} <span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
</button> </button>
{% elif order.status == PurchaseOrderStatus.PLACED %} {% elif order.status == PurchaseOrderStatus.PLACED %}
<button type='button' class='btn btn-success' id='receive-selected-items' title='{% trans "Receive selected items" %}'> <button type='button' class='btn btn-primary' id='receive-selected-items' title='{% trans "Receive selected items" %}'>
<span class='fas fa-sign-in-alt'></span> {% trans "Receive Items" %} <span class='fas fa-sign-in-alt'></span> {% trans "Receive Items" %}
</button> </button>
{% endif %} {% endif %}
@ -124,51 +124,16 @@
} }
); );
loadAttachmentTable( loadAttachmentTable('{% url "api-po-attachment-list" %}', {
'{% url "api-po-attachment-list" %}',
{
filters: { filters: {
order: {{ order.pk }}, order: {{ order.pk }},
}, },
onEdit: function(pk) {
var url = `/api/order/po/attachment/${pk}/`;
constructForm(url, {
fields: { fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/order/po/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$("#new-attachment").click(function() {
constructForm('{% url "api-po-attachment-list" %}', {
method: 'POST',
fields: {
attachment: {},
comment: {},
order: { order: {
value: {{ order.pk }}, value: {{ order.pk }},
hidden: true, hidden: true,
}, }
}, }
reload: true,
title: '{% trans "Add Attachment" %}',
});
}); });
loadStockTable($("#stock-table"), { loadStockTable($("#stock-table"), {

View File

@ -68,17 +68,33 @@ src="{% static 'img/blank_image.png' %}"
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock actions %}
{% block details %} {% block details %}
<h4> <table class='table table-striped table-condensed'>
{% sales_order_status_label order.status large=True %} <col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Order Description" %}</td>
<td>{{ order.description }}{% include "clip.html" %}</td>
</tr>
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
<td>
{% sales_order_status_label order.status %}
{% if order.is_overdue %} {% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span> <span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %} {% endif %}
</h4> </td>
<p>{{ order.description }}{% include "clip.html"%}</p> </tr>
</table>
<div class='info-messages'> <div class='info-messages'>
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %} {% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
@ -93,21 +109,6 @@ src="{% static 'img/blank_image.png' %}"
{% block details_right %} {% block details_right %}
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<col width='25'> <col width='25'>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Order Reference" %}</td>
<td>{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
<td>
{% sales_order_status_label order.status %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}
</td>
</tr>
{% if order.customer %} {% if order.customer %}
<tr> <tr>
<td><span class='fas fa-building'></span></td> <td><span class='fas fa-building'></span></td>

View File

@ -110,55 +110,21 @@
}, },
label: 'attachment', label: 'attachment',
success: function(data, status, xhr) { success: function(data, status, xhr) {
location.reload(); reloadAttachmentTable();
} }
} }
); );
loadAttachmentTable( loadAttachmentTable('{% url "api-so-attachment-list" %}', {
'{% url "api-so-attachment-list" %}',
{
filters: { filters: {
order: {{ order.pk }}, order: {{ order.pk }},
}, },
onEdit: function(pk) {
var url = `/api/order/so/attachment/${pk}/`;
constructForm(url, {
fields: { fields: {
filename: {},
comment: {},
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Edit Attachment" %}',
});
},
onDelete: function(pk) {
constructForm(`/api/order/so/attachment/${pk}/`, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$("#new-attachment").click(function() {
constructForm('{% url "api-so-attachment-list" %}', {
method: 'POST',
fields: {
attachment: {},
comment: {},
order: { order: {
value: {{ order.pk }}, value: {{ order.pk }},
hidden: true hidden: true,
}
}, },
onSuccess: reloadAttachmentTable, }
title: '{% trans "Add Attachment" %}'
});
}); });
loadBuildTable($("#builds-table"), { loadBuildTable($("#builds-table"), {

View File

@ -2,7 +2,11 @@
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %} {% trans "Line Items" as text %}
{% include "sidebar_item.html" with label='order-builds' text="Build Orders" icon="fa-tools" %} {% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %} {% trans "Build Orders" as text %}
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %} {% include "sidebar_item.html" with label='order-builds' text=text icon="fa-tools" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}

View File

@ -105,6 +105,25 @@ class PurchaseOrderTest(OrderTest):
self.assertEqual(data['pk'], 1) self.assertEqual(data['pk'], 1)
self.assertEqual(data['description'], 'Ordering some screws') self.assertEqual(data['description'], 'Ordering some screws')
def test_po_reference(self):
"""test that a reference with a too big / small reference is not possible"""
# get permissions
self.assignRole('purchase_order.add')
url = reverse('api-po-list')
huge_numer = 9223372036854775808
# too big
self.post(
url,
{
'supplier': 1,
'reference': huge_numer,
'description': 'PO not created via the API',
},
expected_code=400
)
def test_po_attachments(self): def test_po_attachments(self):
url = reverse('api-po-attachment-list') url = reverse('api-po-attachment-list')

View File

@ -205,7 +205,7 @@ class BomItemResource(ModelResource):
# If we are not generating an "import" template, # If we are not generating an "import" template,
# just return the complete list of fields # just return the complete list of fields
if not self.is_importing: if not getattr(self, 'is_importing', False):
return fields return fields
# Otherwise, remove some fields we are not interested in # Otherwise, remove some fields we are not interested in

View File

@ -26,7 +26,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from .models import Part, PartCategory from .models import Part, PartCategory, PartRelated
from .models import BomItem, BomItemSubstitute from .models import BomItem, BomItemSubstitute
from .models import PartParameter, PartParameterTemplate from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment, PartTestTemplate from .models import PartAttachment, PartTestTemplate
@ -42,7 +42,7 @@ from build.models import Build
from . import serializers as part_serializers from . import serializers as part_serializers
from InvenTree.helpers import str2bool, isNull from InvenTree.helpers import str2bool, isNull, increment
from InvenTree.api import AttachmentMixin from InvenTree.api import AttachmentMixin
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatus
@ -410,6 +410,33 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
] ]
class PartSerialNumberDetail(generics.RetrieveAPIView):
"""
API endpoint for returning extra serial number information about a particular part
"""
queryset = Part.objects.all()
def retrieve(self, request, *args, **kwargs):
part = self.get_object()
# Calculate the "latest" serial number
latest = part.getLatestSerialNumber()
data = {
'latest': latest,
}
if latest is not None:
next = increment(latest)
if next != increment:
data['next'] = next
return Response(data)
class PartDetail(generics.RetrieveUpdateDestroyAPIView): class PartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single Part object """ """ API endpoint for detail view of a single Part object """
@ -901,6 +928,40 @@ class PartList(generics.ListCreateAPIView):
queryset = queryset.filter(pk__in=pks) queryset = queryset.filter(pk__in=pks)
# Filter by 'related' parts?
related = params.get('related', None)
exclude_related = params.get('exclude_related', None)
if related is not None or exclude_related is not None:
try:
pk = related if related is not None else exclude_related
pk = int(pk)
related_part = Part.objects.get(pk=pk)
part_ids = set()
# Return any relationship which points to the part in question
relation_filter = Q(part_1=related_part) | Q(part_2=related_part)
for relation in PartRelated.objects.filter(relation_filter):
if relation.part_1.pk != pk:
part_ids.add(relation.part_1.pk)
if relation.part_2.pk != pk:
part_ids.add(relation.part_2.pk)
if related is not None:
# Only return related results
queryset = queryset.filter(pk__in=[pk for pk in part_ids])
elif exclude_related is not None:
# Exclude related results
queryset = queryset.exclude(pk__in=[pk for pk in part_ids])
except (ValueError, Part.DoesNotExist):
pass
# Filter by 'starred' parts? # Filter by 'starred' parts?
starred = params.get('starred', None) starred = params.get('starred', None)
@ -1014,9 +1075,48 @@ class PartList(generics.ListCreateAPIView):
'revision', 'revision',
'keywords', 'keywords',
'category__name', 'category__name',
'manufacturer_parts__MPN',
] ]
class PartRelatedList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of PartRelated objects
"""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Add a filter for "part" - we can filter either part_1 or part_2
part = params.get('part', None)
if part is not None:
try:
part = Part.objects.get(pk=part)
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part))
except (ValueError, Part.DoesNotExist):
pass
return queryset
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for accessing detail view of a PartRelated object
"""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
class PartParameterTemplateList(generics.ListCreateAPIView): class PartParameterTemplateList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameterTemplate objects. """ API endpoint for accessing a list of PartParameterTemplate objects.
@ -1081,24 +1181,6 @@ class BomFilter(rest_filters.FilterSet):
inherited = rest_filters.BooleanFilter(label='BOM line is inherited') inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed') allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
def filter_validated(self, queryset, name, value):
# Work out which lines have actually been validated
pks = []
for bom_item in queryset.all():
if bom_item.is_line_valid():
pks.append(bom_item.pk)
if str2bool(value):
queryset = queryset.filter(pk__in=pks)
else:
queryset = queryset.exclude(pk__in=pks)
return queryset
# Filters for linked 'part' # Filters for linked 'part'
part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active') part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active')
part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable') part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable')
@ -1107,6 +1189,30 @@ class BomFilter(rest_filters.FilterSet):
sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable') sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable')
sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly') sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly')
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
def filter_validated(self, queryset, name, value):
# Work out which lines have actually been validated
pks = []
value = str2bool(value)
# Shortcut for quicker filtering - BomItem with empty 'checksum' values are not validated
if value:
queryset = queryset.exclude(checksum=None).exclude(checksum='')
for bom_item in queryset.all():
if bom_item.is_line_valid:
pks.append(bom_item.pk)
if value:
queryset = queryset.filter(pk__in=pks)
else:
queryset = queryset.exclude(pk__in=pks)
return queryset
class BomList(generics.ListCreateAPIView): class BomList(generics.ListCreateAPIView):
""" """
@ -1435,6 +1541,12 @@ part_api_urls = [
url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'), url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
])), ])),
# Base URL for PartRelated API endpoints
url(r'^related/', include([
url(r'^(?P<pk>\d+)/', PartRelatedDetail.as_view(), name='api-part-related-detail'),
url(r'^.*$', PartRelatedList.as_view(), name='api-part-related-list'),
])),
# Base URL for PartParameter API endpoints # Base URL for PartParameter API endpoints
url(r'^parameter/', include([ url(r'^parameter/', include([
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'), url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
@ -1448,7 +1560,14 @@ part_api_urls = [
url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'), url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
])), ])),
url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'), url(r'^(?P<pk>\d+)/', include([
# Endpoint for extra serial number information
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
# Part detail endpoint
url(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
])),
url(r'^.*$', PartList.as_view(), name='api-part-list'), url(r'^.*$', PartList.as_view(), name='api-part-list'),
] ]

View File

@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
uids = [] uids = []
def add_items(items, level, cascade): def add_items(items, level, cascade=True):
# Add items at a given layer # Add items at a given layer
for item in items: for item in items:

View File

@ -17,7 +17,7 @@ from InvenTree.fields import RoundingDecimalFormField
import common.models import common.models
from common.forms import MatchItemForm from common.forms import MatchItemForm
from .models import Part, PartCategory, PartRelated from .models import Part, PartCategory
from .models import PartParameterTemplate from .models import PartParameterTemplate
from .models import PartCategoryParameterTemplate from .models import PartCategoryParameterTemplate
from .models import PartSellPriceBreak, PartInternalPriceBreak from .models import PartSellPriceBreak, PartInternalPriceBreak
@ -157,20 +157,6 @@ class BomMatchItemForm(MatchItemForm):
return super().get_special_field(col_guess, row, file_manager) return super().get_special_field(col_guess, row, file_manager)
class CreatePartRelatedForm(HelperForm):
""" Form for creating a PartRelated object """
class Meta:
model = PartRelated
fields = [
'part_1',
'part_2',
]
labels = {
'part_2': _('Related Part'),
}
class SetPartCategoryForm(forms.Form): class SetPartCategoryForm(forms.Form):
""" Form for setting the category of multiple Part objects """ """ Form for setting the category of multiple Part objects """

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0074_partcategorystar'),
]
operations = [
migrations.AddField(
model_name='partattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='partattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -1587,7 +1587,7 @@ class Part(MPTTModel):
# Exclude any parts that this part is used *in* (to prevent recursive BOMs) # Exclude any parts that this part is used *in* (to prevent recursive BOMs)
used_in = self.get_used_in().all() used_in = self.get_used_in().all()
parts = parts.exclude(id__in=[item.part.id for item in used_in]) parts = parts.exclude(id__in=[part.id for part in used_in])
return parts return parts

View File

@ -25,7 +25,7 @@ from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
from stock.models import StockItem from stock.models import StockItem
from .models import (BomItem, BomItemSubstitute, from .models import (BomItem, BomItemSubstitute,
Part, PartAttachment, PartCategory, Part, PartAttachment, PartCategory, PartRelated,
PartParameter, PartParameterTemplate, PartSellPriceBreak, PartParameter, PartParameterTemplate, PartSellPriceBreak,
PartStar, PartTestTemplate, PartCategoryParameterTemplate, PartStar, PartTestTemplate, PartCategoryParameterTemplate,
PartInternalPriceBreak) PartInternalPriceBreak)
@ -75,8 +75,6 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
Serializer for the PartAttachment class Serializer for the PartAttachment class
""" """
attachment = InvenTreeAttachmentSerializerField(required=True)
class Meta: class Meta:
model = PartAttachment model = PartAttachment
@ -85,6 +83,7 @@ class PartAttachmentSerializer(InvenTreeAttachmentSerializer):
'part', 'part',
'attachment', 'attachment',
'filename', 'filename',
'link',
'comment', 'comment',
'upload_date', 'upload_date',
] ]
@ -388,6 +387,25 @@ class PartSerializer(InvenTreeModelSerializer):
] ]
class PartRelationSerializer(InvenTreeModelSerializer):
"""
Serializer for a PartRelated model
"""
part_1_detail = PartSerializer(source='part_1', read_only=True, many=False)
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False)
class Meta:
model = PartRelated
fields = [
'pk',
'part_1',
'part_1_detail',
'part_2',
'part_2_detail',
]
class PartStarSerializer(InvenTreeModelSerializer): class PartStarSerializer(InvenTreeModelSerializer):
""" Serializer for a PartStar object """ """ Serializer for a PartStar object """

View File

@ -5,7 +5,8 @@
{% block sidebar %} {% block sidebar %}
{% url "part-detail" part.id as url %} {% url "part-detail" part.id as url %}
{% include "sidebar_link.html" with url=url text="Return to BOM" icon="fa-undo" %} {% trans "Return to BOM" as text %}
{% include "sidebar_link.html" with url=url text=text icon="fa-undo" %}
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}

View File

@ -61,29 +61,43 @@
{% endblock %} {% endblock %}
{% block details_left %} {% block details_left %}
{% if category %} <table class='table table-striped table-condensed'>
<p>{{ category.description }}</p> <col width='25'>
{% else %} {% if category %}
<p>{% trans "Top level part category" %}</p> {% if category.description %}
{% endif %} <tr>
<td><span class='fas fa-info-circle'></span></td>
{% endblock %} <td>{% trans "Description" %}</td>
<td>{{ category.description }}</td>
</tr>
{% endif %}
<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 %} {% block details_right %}
{% if category %} {% if category %}
<table class='table table-condensed table-striped'> <table class='table table-condensed table-striped'>
<col width='25'> <col width='25'>
<tr>
<td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Category Path" %}</td>
<td>{{ category.pathstring }}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Category Description" %}</td>
<td>{{ category.description }}</td>
</tr>
{% if category.default_location %} {% if category.default_location %}
<tr> <tr>
<td><span class='fas fa-map-marker-alt'></span></td> <td><span class='fas fa-map-marker-alt'></span></td>
@ -91,13 +105,6 @@
<td><a href="{% url 'stock-location-detail' category.default_location.pk %}">{{ category.default_location.pathstring }}</a></td> <td><a href="{% url 'stock-location-detail' category.default_location.pk %}">{{ category.default_location.pathstring }}</a></td>
</tr> </tr>
{% endif %} {% endif %}
{% if category.default_keywords %}
<tr>
<td><span class='fas fa-key'></span></td>
<td>{% trans "Keywords" %}</td>
<td>{{ category.default_keywords }}</td>
</tr>
{% endif %}
<tr> <tr>
<td><span class='fas fa-sitemap'></span></td> <td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Subcategories" %}</td> <td>{% trans "Subcategories" %}</td>
@ -124,7 +131,7 @@
</tr> </tr>
</table> </table>
{% endif %} {% endif %}
{% endblock %} {% endblock details_right %}
{% block page_content %} {% block page_content %}

View File

@ -4,12 +4,16 @@
{% settings_value 'PART_SHOW_IMPORT' as show_import %} {% settings_value 'PART_SHOW_IMPORT' as show_import %}
{% include "sidebar_item.html" with label="subcategories" text="Subcategories" icon="fa-sitemap" %} {% trans "Subcategories" as text %}
{% include "sidebar_item.html" with label="parts" text="Parts" icon="fa-shapes" %} {% include "sidebar_item.html" with label="subcategories" text=text icon="fa-sitemap" %}
{% trans "Parts" as text %}
{% include "sidebar_item.html" with label="parts" text=text icon="fa-shapes" %}
{% if show_import and user.is_staff and roles.part.add %} {% if show_import and user.is_staff and roles.part.add %}
{% url "part-import" as url %} {% url "part-import" as url %}
{% include "sidebar_link.html" with url=url text="Import Parts" icon="fa-file-upload" %} {% trans "Import Parts" as text %}
{% include "sidebar_link.html" with url=url text=text icon="fa-file-upload" %}
{% endif %} {% endif %}
{% if category %} {% if category %}
{% include "sidebar_item.html" with label="parameters" text="Parameters" icon="fa-tasks" %} {% trans "Parameters" as text %}
{% include "sidebar_item.html" with label="parameters" text=text icon="fa-tasks" %}
{% endif %} {% endif %}

View File

@ -11,113 +11,6 @@
{% block page_content %} {% block page_content %}
<div class='panel panel-hidden' id='panel-part-details'>
<div class='panel-heading'>
<h4>{% trans "Part Details" %}</h4>
</div>
<div class='panel-content'>
<!-- Details Table -->
<table class="table table-striped table-condensed">
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Name" %}</td>
<td>{{ part.name }}{% include "clip.html"%}</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
{% if part.category %}
<tr>
<td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Category" %}</td>
<td>
<a href='{% url "category-detail" part.category.pk %}'>{{ part.category.name }}</a>
</td>
</tr>
{% endif %}
{% if part.IPN %}
<tr>
<td><span class='fas fa-tag'></span></td>
<td>{% trans "IPN" %}</td>
<td>{{ part.IPN }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.revision %}
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Revision" %}</td>
<td>{{ part.revision }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.units %}
<tr>
<td></td>
<td>{% trans "Units" %}</td>
<td>{{ part.units }}</td>
</tr>
{% endif %}
{% if part.minimum_stock %}
<tr>
<td><span class='fas fa-flag'></span></td>
<td>{% trans "Minimum stock level" %}</td>
<td>{{ part.minimum_stock }}</td>
</tr>
{% endif %}
{% if part.keywords %}
<tr>
<td><span class='fas fa-key'></span></td>
<td>{% trans "Keywords" %}</td>
<td>{{ part.keywords }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Creation Date" %}</td>
<td>
{{ part.creation_date }}
{% if part.creation_user %}
<span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span>
{% endif %}
</td>
</tr>
{% if part.trackable and part.getLatestSerialNumber %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Latest Serial Number" %}</td>
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.default_location %}
<tr>
<td><span class='fas fa-search-location'></span></td>
<td>{% trans "Default Location" %}</td>
<td>
<a href='{% url "stock-location-detail" part.default_location.pk %}'>{{ part.default_location }}</a>
</td>
</tr>
{% endif %}
{% if part.default_supplier %}
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Default Supplier" %}</td>
<td>{{ part.default_supplier }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class='panel panel-hidden' id='panel-part-stock'> <div class='panel panel-hidden' id='panel-part-stock'>
<div class='panel-heading'> <div class='panel-heading'>
<div class='d-flex flex-wrap'> <div class='d-flex flex-wrap'>
@ -330,33 +223,7 @@
</div> </div>
</div> </div>
<table id='table-related-part' class='table table-condensed table-striped' data-toolbar='#related-button-toolbar'> <table id='related-parts-table' class='table table-striped table-condensed' data-toolbar='#related-button-toolbar'></table>
<thead>
<tr>
<th data-field='part' data-serachable='true'>{% trans "Part" %}</th>
</tr>
</thead>
<tbody>
{% for item in part.get_related_parts %}
{% with part_related=item.0 part=item.1 %}
<tr>
<td>
<a class='hover-icon'>
<img class='hover-img-thumb' src='{{ part.get_thumbnail_url }}'>
<img class='hover-img-large' src='{{ part.get_thumbnail_url }}'>
</a>
<a href='/part/{{ part.id }}/'>{{ part }}</a>
<div class='btn-group' style='float: right;'>
{% if roles.part.change %}
<button title='{% trans "Delete" %}' class='btn btn-outline-secondary delete-related-part' url="{% url 'part-related-delete' part_related.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
{% endif %}
</div>
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
@ -771,15 +638,34 @@
// Load the "related parts" tab // Load the "related parts" tab
onPanelLoad("related-parts", function() { onPanelLoad("related-parts", function() {
$('#table-related-part').inventreeTable({
}); loadRelatedPartsTable(
"#related-parts-table",
{{ part.pk }}
);
$("#add-related-part").click(function() { $("#add-related-part").click(function() {
launchModalForm("{% url 'part-related-create' %}", {
data: { constructForm('{% url "api-part-related-list" %}', {
part: {{ part.id }}, method: 'POST',
fields: {
part_1: {
hidden: true,
value: {{ part.pk }},
}, },
reload: true, part_2: {
label: '{% trans "Related Part" %}',
filters: {
exclude_id: {{ part.pk }},
exclude_related: {{ part.pk }},
}
}
},
focus: 'part_2',
title: '{% trans "Add Related Part" %}',
onSuccess: function() {
$('#related-parts-table').bootstrapTable('refresh');
}
}); });
}); });
@ -1006,36 +892,17 @@
}); });
onPanelLoad("part-attachments", function() { onPanelLoad("part-attachments", function() {
loadAttachmentTable( loadAttachmentTable('{% url "api-part-attachment-list" %}', {
'{% url "api-part-attachment-list" %}',
{
filters: { filters: {
part: {{ part.pk }}, part: {{ part.pk }},
}, },
onEdit: function(pk) {
var url = `/api/part/attachment/${pk}/`;
constructForm(url, {
fields: { fields: {
filename: {}, part: {
comment: {}, value: {{ part.pk }},
}, hidden: true
title: '{% trans "Edit Attachment" %}',
onSuccess: reloadAttachmentTable,
});
},
onDelete: function(pk) {
var url = `/api/part/attachment/${pk}/`;
constructForm(url, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
} }
} }
); });
enableDragAndDrop( enableDragAndDrop(
'#attachment-dropzone', '#attachment-dropzone',
@ -1050,26 +917,6 @@
} }
} }
); );
$("#new-attachment").click(function() {
constructForm(
'{% url "api-part-attachment-list" %}',
{
method: 'POST',
fields: {
attachment: {},
comment: {},
part: {
value: {{ part.pk }},
hidden: true,
}
},
onSuccess: reloadAttachmentTable,
title: '{% trans "Add Attachment" %}',
}
)
});
}); });

View File

@ -5,7 +5,8 @@
{% block sidebar %} {% block sidebar %}
{% url 'part-index' as url %} {% url 'part-index' as url %}
{% include "sidebar_link.html" with url=url text="Return to Parts" icon="fa-undo" %} {% trans "Return to Parts" as text %}
{% include "sidebar_link.html" with url=url text=text icon="fa-undo" %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -99,11 +99,14 @@
{% block details %} {% block details %}
</h4>
<!-- Properties --> <!-- Properties -->
<h4> <table class='table table-striped table-condensed' id='part-info-table'>
<div id='part-properties' class='btn-group'> <col width='25'>
<tr>
<td colspan='3' style='padding: 3px;'>
<div id='part-properties-wrapper' class='d-flex flex-wrap'>
<div id='part-properties' class='btn-group' role='group';'>
<h5>
{% if part.is_template %} {% if part.is_template %}
&ensp; &ensp;
<span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span> <span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span>
@ -144,8 +147,23 @@
{% trans 'Virtual' %} {% trans 'Virtual' %}
</div> </div>
{% endif %} {% endif %}
</h5>
</div> </div>
</h4>
{% include "spacer.html" %}
<button type='button' class='btn btn-outline-secondary' data-bs-toggle='collapse' href='#collapse-part-details' role='button' id='toggle-details-button'>
{% trans "Show Part Details" %}
</button>
</div>
</td>
</tr>
<tr>
<td><span class='fas fa-info-circle'></span></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}{% include "clip.html"%}</td>
</tr>
</table>
<!-- Part info messages --> <!-- Part info messages -->
<div class='info-messages'> <div class='info-messages'>
@ -157,7 +175,7 @@
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock details %}
{% block details_right %} {% block details_right %}
<table class='table table-condensed table-striped'> <table class='table table-condensed table-striped'>
@ -231,7 +249,118 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</table> </table>
{% endblock %} {% endblock details_right %}
{% block details_below %}
<!-- Part Details -->
<div class='collapse' id='collapse-part-details'>
<div class='row flex-wrap'>
<div class='col-sm-6'>
<!-- Details Table -->
<table class="table table-striped table-condensed">
<col width='25'>
{% if part.category %}
<tr>
<td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Category" %}</td>
<td>
<a href='{% url "category-detail" part.category.pk %}'>{{ part.category.name }}</a>
</td>
</tr>
{% endif %}
{% if part.IPN %}
<tr>
<td><span class='fas fa-tag'></span></td>
<td>{% trans "IPN" %}</td>
<td>{{ part.IPN }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.revision %}
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Revision" %}</td>
<td>{{ part.revision }}{% include "clip.html"%}</td>
</tr>
{% endif %}
{% if part.units %}
<tr>
<td></td>
<td>{% trans "Units" %}</td>
<td>{{ part.units }}</td>
</tr>
{% endif %}
{% if part.minimum_stock %}
<tr>
<td><span class='fas fa-flag'></span></td>
<td>{% trans "Minimum stock level" %}</td>
<td>{{ part.minimum_stock }}</td>
</tr>
{% endif %}
{% if part.keywords %}
<tr>
<td><span class='fas fa-key'></span></td>
<td>{% trans "Keywords" %}</td>
<td>{{ part.keywords }}{% include "clip.html"%}</td>
</tr>
{% endif %}
</table>
</div>
<div class='col-sm-6'>
<table class="table table-striped table-condensed">
<col width='25'>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Creation Date" %}</td>
<td>
{{ part.creation_date }}
{% if part.creation_user %}
<span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span>
{% endif %}
</td>
</tr>
{% if part.trackable and part.getLatestSerialNumber %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Latest Serial Number" %}</td>
<td>
{{ part.getLatestSerialNumber }}
<div class='btn-group float-right' role='group'>
<a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
<span class='fas fa-search'></span>
</a>
</div>
</td>
</tr>
{% endif %}
{% if part.default_location %}
<tr>
<td><span class='fas fa-search-location'></span></td>
<td>{% trans "Default Location" %}</td>
<td>
<a href='{% url "stock-location-detail" part.default_location.pk %}'>{{ part.default_location }}</a>
</td>
</tr>
{% endif %}
{% if part.default_supplier %}
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Default Supplier" %}</td>
<td>{{ part.default_supplier }}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
</tr>
{% endif %}
</table>
</div>
</div>
</div>
{% endblock details_below %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
@ -439,4 +568,24 @@
}); });
{% endif %} {% endif %}
// Callback function when the "part details" panel is shown
$('#collapse-part-details').on('show.bs.collapse', function() {
$('#toggle-details-button').html('{% trans "Hide Part Details" %}');
inventreeSave('show-part-details', true);
});
// Callback function when the "part details" panel is hidden
$('#collapse-part-details').on('hide.bs.collapse', function() {
$('#toggle-details-button').html('{% trans "Show Part Details" %}');
inventreeSave('show-part-details', false);
});
if (inventreeLoad('show-part-details', false).toString() == 'true') {
$('#collapse-part-details').collapse('show');
}
$('#serial-number-search').click(function() {
findStockItemBySerialNumber({{ part.pk }});
});
{% endblock %} {% endblock %}

View File

@ -5,34 +5,47 @@
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} {% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
{% settings_value 'PART_SHOW_RELATED' as show_related %} {% settings_value 'PART_SHOW_RELATED' as show_related %}
{% include "sidebar_item.html" with label="part-details" text="Details" icon="fa-shapes" %} {% trans "Parameters" as text %}
{% include "sidebar_item.html" with label="part-parameters" text="Parameters" icon="fa-th-list" %} {% include "sidebar_item.html" with label="part-parameters" text=text icon="fa-th-list" %}
{% if part.is_template %} {% if part.is_template %}
{% include "sidebar_item.html" with label="variants" text="Variants" icon="fa-shapes" %} {% trans "Variants" as text %}
{% include "sidebar_item.html" with label="variants" text=text icon="fa-shapes" %}
{% endif %} {% endif %}
{% include "sidebar_item.html" with label="part-stock" text="Stock" icon="fa-boxes" %} {% trans "Stock" as text %}
{% include "sidebar_item.html" with label="part-stock" text=text icon="fa-boxes" %}
{% if part.assembly %} {% if part.assembly %}
{% include "sidebar_item.html" with label="bom" text="Bill of Materials" icon="fa-list" %} {% trans "Bill of Materials" as text %}
{% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %}
{% if roles.build.view %} {% if roles.build.view %}
{% include "sidebar_item.html" with label="build-orders" text="Build Orders" icon="fa-tools" %} {% trans "Build Orders" as text %}
{% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if part.component %} {% if part.component %}
{% include "sidebar_item.html" with label="used-in" text="Used In" icon="fa-layer-group" %} {% trans "Used In" as text %}
{% include "sidebar_item.html" with label="used-in" text=text icon="fa-layer-group" %}
{% endif %} {% endif %}
{% include "sidebar_item.html" with label="pricing" text="Pricing" icon="fa-dollar-sign" %} {% trans "Pricing" as text %}
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
{% if part.purchaseable and roles.purchase_order.view %} {% if part.purchaseable and roles.purchase_order.view %}
{% include "sidebar_item.html" with label="suppliers" text="Suppliers" icon="fa-building" %} {% trans "Suppliers" as text %}
{% include "sidebar_item.html" with label="purchase-orders" text="Purchase Orders" icon="fa-shopping-cart" %} {% include "sidebar_item.html" with label="suppliers" text=text icon="fa-building" %}
{% trans "Purchase Orders" as text %}
{% include "sidebar_item.html" with label="purchase-orders" text=text icon="fa-shopping-cart" %}
{% endif %} {% endif %}
{% if part.salable and roles.sales_order.view %} {% if part.salable and roles.sales_order.view %}
{% include "sidebar_item.html" with label="sales-orders" text="Sales Orders" icon="fa-truck" %} {% trans "Sales Orders" as text %}
{% include "sidebar_item.html" with label="sales-orders" text=text icon="fa-truck" %}
{% endif %} {% endif %}
{% if part.trackable %} {% if part.trackable %}
{% include "sidebar_item.html" with label="test-templates" text="Test Templates" icon="fa-vial" %} {% trans "Test Templates" as text %}
{% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %}
{% endif %} {% endif %}
{% if show_related %} {% if show_related %}
{% include "sidebar_item.html" with label="related-parts" text="Related Parts" icon="fa-random" %} {% trans "Related Parts" as text %}
{% include "sidebar_item.html" with label="related-parts" text=text icon="fa-random" %}
{% endif %} {% endif %}
{% include "sidebar_item.html" with label="part-attachments" text="Attachments" icon="fa-paperclip" %} {% trans "Attachments" as text %}
{% include "sidebar_item.html" with label="part-notes" text="Notes" icon="fa-clipboard" %} {% include "sidebar_item.html" with label="part-attachments" text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label="part-notes" text=text icon="fa-clipboard" %}

View File

@ -925,7 +925,46 @@ class BomItemTest(InvenTreeAPITestCase):
expected_code=200 expected_code=200
) )
print("results:", len(response.data)) # Filter by "validated"
response = self.get(
url,
data={
'validated': True,
},
expected_code=200,
)
# Should be zero validated results
self.assertEqual(len(response.data), 0)
# Now filter by "not validated"
response = self.get(
url,
data={
'validated': False,
},
expected_code=200
)
# There should be at least one non-validated item
self.assertTrue(len(response.data) > 0)
# Now, let's validate an item
bom_item = BomItem.objects.first()
bom_item.validate_hash()
response = self.get(
url,
data={
'validated': True,
},
expected_code=200
)
# Check that the expected response is returned
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['pk'], bom_item.pk)
def test_get_bom_detail(self): def test_get_bom_detail(self):
""" """

View File

@ -5,7 +5,7 @@ from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from .models import Part, PartRelated from .models import Part
class PartViewTestCase(TestCase): class PartViewTestCase(TestCase):
@ -145,36 +145,6 @@ class PartDetailTest(PartViewTestCase):
self.assertIn('streaming_content', dir(response)) self.assertIn('streaming_content', dir(response))
class PartRelatedTests(PartViewTestCase):
def test_valid_create(self):
""" test creation of a related part """
# Test GET view
response = self.client.get(reverse('part-related-create'), {'part': 1},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Test POST view with valid form data
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 2},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": true', status_code=200)
# Try to create the same relationship with part_1 and part_2 pks reversed
response = self.client.post(reverse('part-related-create'), {'part_1': 2, 'part_2': 1},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200)
# Try to create part related to itself
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 1},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, '"form_valid": false', status_code=200)
# Check final count
n = PartRelated.objects.all().count()
self.assertEqual(n, 1)
class PartQRTest(PartViewTestCase): class PartQRTest(PartViewTestCase):
""" Tests for the Part QR Code AJAX view """ """ Tests for the Part QR Code AJAX view """

View File

@ -12,10 +12,6 @@ from django.conf.urls import url, include
from . import views from . import views
part_related_urls = [
url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'),
url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'),
]
sale_price_break_urls = [ sale_price_break_urls = [
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'), url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
@ -96,9 +92,6 @@ part_urls = [
# Part category # Part category
url(r'^category/', include(category_urls)), url(r'^category/', include(category_urls)),
# Part related
url(r'^related-parts/', include(part_related_urls)),
# Part price breaks # Part price breaks
url(r'^sale-price/', include(sale_price_break_urls)), url(r'^sale-price/', include(sale_price_break_urls)),

View File

@ -30,7 +30,7 @@ import io
from rapidfuzz import fuzz from rapidfuzz import fuzz
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from .models import PartCategory, Part, PartRelated from .models import PartCategory, Part
from .models import PartParameterTemplate from .models import PartParameterTemplate
from .models import PartCategoryParameterTemplate from .models import PartCategoryParameterTemplate
from .models import BomItem from .models import BomItem
@ -85,75 +85,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
return context return context
class PartRelatedCreate(AjaxCreateView):
""" View for creating a new PartRelated object
- The view only makes sense if a Part object is passed to it
"""
model = PartRelated
form_class = part_forms.CreatePartRelatedForm
ajax_form_title = _("Add Related Part")
ajax_template_name = "modal_form.html"
def get_initial(self):
""" Set parent part as part_1 field """
initials = {}
part_id = self.request.GET.get('part', None)
if part_id:
try:
initials['part_1'] = Part.objects.get(pk=part_id)
except (Part.DoesNotExist, ValueError):
pass
return initials
def get_form(self):
""" Create a form to upload a new PartRelated
- Hide the 'part_1' field (parent part)
- Display parts which are not yet related
"""
form = super(AjaxCreateView, self).get_form()
form.fields['part_1'].widget = HiddenInput()
try:
# Get parent part
parent_part = self.get_initial()['part_1']
# Get existing related parts
related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()]
# Build updated choice list excluding
# - parts already related to parent part
# - the parent part itself
updated_choices = []
for choice in form.fields["part_2"].choices:
if (choice[0] not in related_parts) and (choice[0] != parent_part.pk):
updated_choices.append(choice)
# Update choices for related part
form.fields['part_2'].choices = updated_choices
except KeyError:
pass
return form
class PartRelatedDelete(AjaxDeleteView):
""" View for deleting a PartRelated object """
model = PartRelated
ajax_form_title = _("Delete Related Part")
context_object_name = "related"
# Explicit role requirement
role_required = 'part.change'
class PartSetCategory(AjaxUpdateView): class PartSetCategory(AjaxUpdateView):
""" View for settings the part category for multiple parts at once """ """ View for settings the part category for multiple parts at once """
@ -508,10 +439,14 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
line['price_part'] = stock_item.supplier_part.unit_pricing line['price_part'] = stock_item.supplier_part.unit_pricing
# set date for graph labels # set date for graph labels
if stock_item.purchase_order: if stock_item.purchase_order and stock_item.purchase_order.issue_date:
line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y') line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y')
else: elif stock_item.tracking_info.count() > 0:
line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y') line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y')
else:
# Not enough information
continue
price_history.append(line) price_history.append(line)
ctx['price_history'] = price_history ctx['price_history'] = price_history

View File

@ -313,7 +313,7 @@ class StockFilter(rest_filters.FilterSet):
# Serial number filtering # Serial number filtering
serial_gte = rest_filters.NumberFilter(label='Serial number GTE', field_name='serial', lookup_expr='gte') serial_gte = rest_filters.NumberFilter(label='Serial number GTE', field_name='serial', lookup_expr='gte')
serial_lte = rest_filters.NumberFilter(label='Serial number LTE', field_name='serial', lookup_expr='lte') serial_lte = rest_filters.NumberFilter(label='Serial number LTE', field_name='serial', lookup_expr='lte')
serial = rest_filters.NumberFilter(label='Serial number', field_name='serial', lookup_expr='exact') serial = rest_filters.CharFilter(label='Serial number', field_name='serial', lookup_expr='exact')
serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized') serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
@ -703,6 +703,18 @@ class StockList(generics.ListCreateAPIView):
except (ValueError, StockItem.DoesNotExist): except (ValueError, StockItem.DoesNotExist):
pass pass
# Filter by "part tree" - only allow parts within a given variant tree
part_tree = params.get('part_tree', None)
if part_tree is not None:
try:
part = Part.objects.get(pk=part_tree)
if part.tree_id is not None:
queryset = queryset.filter(part__tree_id=part.tree_id)
except:
pass
# Filter by 'allocated' parts? # Filter by 'allocated' parts?
allocated = params.get('allocated', None) allocated = params.get('allocated', None)

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-28 01:51
import InvenTree.fields
import InvenTree.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0069_auto_20211109_2347'),
]
operations = [
migrations.AddField(
model_name='stockitemattachment',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True, verbose_name='Link'),
),
migrations.AlterField(
model_name='stockitemattachment',
name='attachment',
field=models.FileField(blank=True, help_text='Select file to attach', null=True, upload_to=InvenTree.models.rename_attachment, verbose_name='Attachment'),
),
]

View File

@ -7,7 +7,6 @@ Stock database model definitions
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import re
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError, FieldError from django.core.exceptions import ValidationError, FieldError
@ -39,6 +38,7 @@ import label.models
from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.status_codes import StockStatus, StockHistoryCode
from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
from InvenTree.serializers import extract_int
from users.models import Owner from users.models import Owner
@ -236,17 +236,7 @@ class StockItem(MPTTModel):
serial_int = 0 serial_int = 0
if serial is not None: if serial is not None:
serial_int = extract_int(str(serial))
serial = str(serial)
# Look at the start of the string - can it be "integerized"?
result = re.match(r'^(\d+)', serial)
if result and len(result.groups()) == 1:
try:
serial_int = int(result.groups()[0])
except:
serial_int = 0
self.serial_int = serial_int self.serial_int = serial_int

View File

@ -32,7 +32,7 @@ from company.serializers import SupplierPartSerializer
import InvenTree.helpers import InvenTree.helpers
import InvenTree.serializers import InvenTree.serializers
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField, extract_int
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
@ -73,6 +73,11 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
'uid', 'uid',
] ]
def validate_serial(self, value):
if extract_int(value) > 2147483647:
raise serializers.ValidationError('serial is to to big')
return value
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Serializer for a StockItem: """ Serializer for a StockItem:
@ -420,8 +425,6 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True) user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True)
# TODO: Record the uploading user when creating or updating an attachment! # TODO: Record the uploading user when creating or updating an attachment!
class Meta: class Meta:
@ -432,6 +435,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
'stock_item', 'stock_item',
'attachment', 'attachment',
'filename', 'filename',
'link',
'comment', 'comment',
'upload_date', 'upload_date',
'user', 'user',

View File

@ -221,55 +221,16 @@
} }
); );
loadAttachmentTable( loadAttachmentTable('{% url "api-stock-attachment-list" %}', {
'{% url "api-stock-attachment-list" %}',
{
filters: { filters: {
stock_item: {{ item.pk }}, stock_item: {{ item.pk }},
}, },
onEdit: function(pk) {
var url = `/api/stock/attachment/${pk}/`;
constructForm(url, {
fields: { fields: {
filename: {},
comment: {},
},
title: '{% trans "Edit Attachment" %}',
onSuccess: reloadAttachmentTable
});
},
onDelete: function(pk) {
var url = `/api/stock/attachment/${pk}/`;
constructForm(url, {
method: 'DELETE',
confirmMessage: '{% trans "Confirm Delete Operation" %}',
title: '{% trans "Delete Attachment" %}',
onSuccess: reloadAttachmentTable,
});
}
}
);
$("#new-attachment").click(function() {
constructForm(
'{% url "api-stock-attachment-list" %}',
{
method: 'POST',
fields: {
attachment: {},
comment: {},
stock_item: { stock_item: {
value: {{ item.pk }}, value: {{ item.pk }},
hidden: true, hidden: true,
},
},
reload: true,
title: '{% trans "Add Attachment" %}',
} }
); }
}); });
loadStockTestResultsTable( loadStockTestResultsTable(

View File

@ -14,7 +14,7 @@
{% block heading %} {% block heading %}
{% trans "Stock Item" %}: {{ item.part.full_name}} {% trans "Stock Item" %}: {{ item.part.full_name}}
{% endblock %} {% endblock heading %}
{% block actions %} {% block actions %}
@ -100,7 +100,9 @@
<!-- Edit stock item --> <!-- Edit stock item -->
{% if roles.stock.change and not item.is_building %} {% if roles.stock.change and not item.is_building %}
<div class='btn-group'> <div class='btn-group'>
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-tools'></span> <span class='caret'></span></button> <button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu' role='menu'> <ul class='dropdown-menu' role='menu'>
{% if item.part.can_convert %} {% if item.part.can_convert %}
<li><a class='dropdown-item' href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li> <li><a class='dropdown-item' href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li>
@ -118,38 +120,101 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock actions %}
{% block thumbnail %} {% block thumbnail %}
<img class='part-thumb' {% if item.part.image %}src="{{ item.part.image.url }}"{% else %}src="{% static 'img/blank_image.png' %}"{% endif %}/> <img class='part-thumb' {% if item.part.image %}src="{{ item.part.image.url }}"{% else %}src="{% static 'img/blank_image.png' %}"{% endif %}/>
{% endblock %} {% endblock thumbnail %}
{% block details %} {% block details %}
<table class='table table-striped table-condensed'>
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Base Part" %}</td>
<td>
{% if roles.part.view %}
<a href="{% url 'part-detail' item.part.id %}">
{% endif %}
{{ item.part.full_name }}
{% if roles.part.view %}
</a>
{% endif %}
</td>
</tr>
{% if item.serialized %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Serial Number" %}</td>
<td>
{{ item.serial }}
<div class='btn-group float-right' role='group'>
{% if previous %}
<a class="btn btn-small btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}" title='{% trans "Navigate to previous serial number" %}'>
<span class='fas fa-angle-left'></span>
<small>{{ previous.serial }}</small>
</a>
{% endif %}
<a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
<span class='fas fa-search'></span>
</a>
{% if next %}
<a class="btn btn-small btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}" title='{% trans "Navigate to next serial number" %}'>
<small>{{ next.serial }}</small>
<span class='fas fa-angle-right'></span>
</a>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>{% stock_status_label item.status %}</td>
</tr>
{% if item.expiry_date %}
<tr>
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
<td>{% trans "Expiry Date" %}</td>
<td>
{{ item.expiry_date }}
{% if item.is_expired %}
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger badge-right'>{% trans "Expired" %}</span>
{% elif item.is_stale %}
<span title='{% blocktrans %}This StockItem expires on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-warning badge-right'>{% trans "Stale" %}</span>
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Updated" %}</td>
<td>{{ item.updated }}</td>
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Stocktake" %}</td>
{% if item.stocktake_date %}
<td>{{ item.stocktake_date }} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
{% else %}
<td><em>{% trans "No stocktake performed" %}</em></td>
{% endif %}
</tr>
</table>
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %} {% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if owner_control.value == "True" %} {% if owner_control.value == "True" %}
{% authorized_owners item.owner as owners %} {% authorized_owners item.owner as owners %}
{% endif %} {% endif %}
<h4>
{% if item.is_expired %}
<span class='badge rounded-pill bg-danger'>{% trans "Expired" %}</span>
{% else %}
{% if roles.stock.change %}
<a href='#' id='stock-edit-status'>
{% endif %}
{% stock_status_label item.status large=True %}
{% if roles.stock.change %}
</a>
{% endif %}
{% if item.is_stale %}
<span class='badge rounded-pill bg-warning'>{% trans "Stale" %}</span>
{% endif %}
{% endif %}
</h4>
<div class='info-messages'> <div class='info-messages'>
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %} {% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
@ -214,49 +279,12 @@
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock details %}
{% block details_right %} {% block details_right %}
<table class="table table-striped"> <table class="table table-striped table-condensed">
<col width='25'> <col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Base Part" %}</td>
<td>
{% if roles.part.view %}
<a href="{% url 'part-detail' item.part.id %}">
{% endif %}
{{ item.part.full_name }}
{% if roles.part.view %}
</a>
{% endif %}
</td>
</tr>
{% if item.serialized %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Serial Number" %}</td>
<td>
{% if previous %}
<a class="btn btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}">
<small>{{ previous.serial }}</small>
</a>
{% endif %}
{{ item.serial }}
{% if next %}
<a class="btn btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}">
<small>{{ next.serial }}</small>
</a>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
</tr>
{% endif %}
{% if item.customer %} {% if item.customer %}
<tr> <tr>
<td><span class='fas fa-user-tie'></span></td> <td><span class='fas fa-user-tie'></span></td>
@ -376,39 +404,6 @@
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td> <td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
</tr> </tr>
{% endif %} {% endif %}
{% if item.expiry_date %}
<tr>
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
<td>{% trans "Expiry Date" %}</td>
<td>
{{ item.expiry_date }}
{% if item.is_expired %}
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger'>{% trans "Expired" %}</span>
{% elif item.is_stale %}
<span title='{% blocktrans %}This StockItem expires on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-warning'>{% trans "Stale" %}</span>
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Updated" %}</td>
<td>{{ item.updated }}</td>
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Stocktake" %}</td>
{% if item.stocktake_date %}
<td>{{ item.stocktake_date }} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
{% else %}
<td><em>{% trans "No stocktake performed" %}</em></td>
{% endif %}
</tr>
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>{% stock_status_label item.status %}</td>
</tr>
{% if item.hasRequiredTests %} {% if item.hasRequiredTests %}
<tr> <tr>
<td><span class='fas fa-vial'></span></td> <td><span class='fas fa-vial'></span></td>
@ -433,6 +428,7 @@
$("#stock-serialize").click(function() { $("#stock-serialize").click(function() {
serializeStockItem({{ item.pk }}, { serializeStockItem({{ item.pk }}, {
part: {{ item.part.pk }},
reload: true, reload: true,
data: { data: {
quantity: {{ item.quantity }}, quantity: {{ item.quantity }},
@ -603,4 +599,8 @@ $("#stock-return-from-customer").click(function() {
{% endif %} {% endif %}
$('#serial-number-search').click(function() {
findStockItemBySerialNumber({{ item.part.pk }});
});
{% endblock %} {% endblock %}

Some files were not shown because too many files have changed in this diff Show More