mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into currency-support
# Conflicts: # InvenTree/InvenTree/settings.py # InvenTree/InvenTree/urls.py # InvenTree/templates/InvenTree/settings/tabs.html # InvenTree/users/models.py # requirements.txt IMPORTANT: Had to merge some migration files due to different migrations applied on the part model tables
This commit is contained in:
commit
cb3c86f87a
13
.travis.yml
13
.travis.yml
@ -30,11 +30,24 @@ before_install:
|
||||
script:
|
||||
- cd InvenTree && python3 manage.py makemigrations && cd ..
|
||||
- python3 ci/check_migration_files.py
|
||||
# Run unit testing / code coverage tests
|
||||
- invoke coverage
|
||||
# Run unit test for SQL database backend
|
||||
- cd InvenTree && python3 manage.py test --settings=InvenTree.ci_mysql && cd ..
|
||||
# Run unit test for PostgreSQL database backend
|
||||
- cd InvenTree && python3 manage.py test --settings=InvenTree.ci_postgresql && cd ..
|
||||
- invoke translate
|
||||
- invoke style
|
||||
# Create an empty database and fill it with test data
|
||||
- rm inventree_default_db.sqlite3
|
||||
- invoke migrate
|
||||
- invoke import-fixtures
|
||||
# Export database records
|
||||
- invoke export-records -f data.json
|
||||
# Create a new empty database and import the saved data
|
||||
- rm inventree_default_db.sqlite3
|
||||
- invoke migrate
|
||||
- invoke import-records -f data.json
|
||||
|
||||
after_success:
|
||||
- coveralls
|
@ -12,6 +12,7 @@ from crispy_forms.layout import Layout, Field
|
||||
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
|
||||
from django.contrib.auth.models import User
|
||||
from common.models import ColorTheme
|
||||
from part.models import PartCategory
|
||||
|
||||
|
||||
class HelperForm(forms.ModelForm):
|
||||
@ -200,3 +201,33 @@ class ColorThemeSelectForm(forms.ModelForm):
|
||||
css_class='row',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SettingCategorySelectForm(forms.ModelForm):
|
||||
""" Form for setting category settings """
|
||||
|
||||
category = forms.ModelChoiceField(queryset=PartCategory.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = PartCategory
|
||||
fields = [
|
||||
'category'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SettingCategorySelectForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.helper = FormHelper()
|
||||
# Form rendering
|
||||
self.helper.form_show_labels = False
|
||||
self.helper.layout = Layout(
|
||||
Div(
|
||||
Div(Field('category'),
|
||||
css_class='col-sm-6',
|
||||
style='width: 70%;'),
|
||||
Div(StrictButton(_('Select Category'), css_class='btn btn-primary', type='submit'),
|
||||
css_class='col-sm-6',
|
||||
style='width: 30%; padding-left: 0;'),
|
||||
css_class='row',
|
||||
),
|
||||
)
|
||||
|
@ -157,6 +157,7 @@ INSTALLED_APPS = [
|
||||
'django_admin_shell', # Python shell for the admin interface
|
||||
'djmoney', # django-money integration
|
||||
'djmoney.contrib.exchange', # django-money exchange rates
|
||||
'error_report', # Error reporting in the admin interface
|
||||
]
|
||||
|
||||
LOGGING = {
|
||||
@ -183,6 +184,9 @@ MIDDLEWARE = CONFIG.get('middleware', [
|
||||
'InvenTree.middleware.AuthRequiredMiddleware'
|
||||
])
|
||||
|
||||
# Error reporting middleware
|
||||
MIDDLEWARE.append('error_report.middleware.ExceptionProcessor')
|
||||
|
||||
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
|
||||
'django.contrib.auth.backends.ModelBackend'
|
||||
])
|
||||
|
@ -36,7 +36,8 @@ from django.views.generic.base import RedirectView
|
||||
from rest_framework.documentation import include_docs_urls
|
||||
|
||||
from .views import IndexView, SearchView, DatabaseStatsView
|
||||
from .views import SettingsView, EditUserView, SetPasswordView, ColorThemeSelectView
|
||||
from .views import SettingsView, EditUserView, SetPasswordView
|
||||
from .views import ColorThemeSelectView, SettingCategorySelectView
|
||||
from .views import DynamicJsView
|
||||
|
||||
from common.views import SettingEdit
|
||||
@ -74,6 +75,7 @@ settings_urls = [
|
||||
url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
|
||||
|
||||
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
||||
url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'),
|
||||
url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'),
|
||||
url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'),
|
||||
url(r'^build/?', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'),
|
||||
@ -125,6 +127,7 @@ urlpatterns = [
|
||||
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
|
||||
url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
|
||||
|
||||
url(r'^admin/error_log/', include('error_report.urls')),
|
||||
url(r'^admin/shell/', include('django_admin_shell.urls')),
|
||||
url(r'^admin/', admin.site.urls, name='inventree-admin'),
|
||||
|
||||
|
@ -24,7 +24,8 @@ from stock.models import StockLocation, StockItem
|
||||
from common.models import InvenTreeSetting, ColorTheme
|
||||
from users.models import check_user_role, RuleSet
|
||||
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm
|
||||
from .forms import ColorThemeSelectForm, SettingCategorySelectForm
|
||||
from .helpers import str2bool
|
||||
|
||||
from rest_framework import views
|
||||
@ -750,6 +751,42 @@ class ColorThemeSelectView(FormView):
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class SettingCategorySelectView(FormView):
|
||||
""" View for selecting categories in settings """
|
||||
|
||||
form_class = SettingCategorySelectForm
|
||||
success_url = reverse_lazy('settings-category')
|
||||
template_name = "InvenTree/settings/category.html"
|
||||
|
||||
def get_initial(self):
|
||||
""" Set category selection """
|
||||
|
||||
initial = super(SettingCategorySelectView, self).get_initial()
|
||||
|
||||
category = self.request.GET.get('category', None)
|
||||
if category:
|
||||
initial['category'] = category
|
||||
|
||||
return initial
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Handle POST request (which contains category selection).
|
||||
|
||||
Pass the selected category to the page template
|
||||
"""
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
if form.is_valid():
|
||||
context = self.get_context_data()
|
||||
|
||||
context['category'] = form.cleaned_data['category']
|
||||
|
||||
return super(SettingCategorySelectView, self).render_to_response(context)
|
||||
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class DatabaseStatsView(AjaxView):
|
||||
""" View for displaying database statistics """
|
||||
|
||||
|
@ -1,92 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.utils import OperationalError, ProgrammingError, IntegrityError
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
name = 'common'
|
||||
|
||||
def ready(self):
|
||||
|
||||
""" Will be called when the Common app is first loaded """
|
||||
self.add_instance_name()
|
||||
self.add_default_settings()
|
||||
|
||||
def add_instance_name(self):
|
||||
"""
|
||||
Check if an InstanceName has been defined for this database.
|
||||
If not, create a random one!
|
||||
"""
|
||||
|
||||
# See note above
|
||||
from .models import InvenTreeSetting
|
||||
|
||||
"""
|
||||
Note: The "old" instance name was stored under the key 'InstanceName',
|
||||
but has now been renamed to 'INVENTREE_INSTANCE'.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
# Quick exit if a value already exists for 'inventree_instance'
|
||||
if InvenTreeSetting.objects.filter(key='INVENTREE_INSTANCE').exists():
|
||||
return
|
||||
|
||||
# Default instance name
|
||||
instance_name = InvenTreeSetting.get_default_value('INVENTREE_INSTANCE')
|
||||
|
||||
# Use the old name if it exists
|
||||
if InvenTreeSetting.objects.filter(key='InstanceName').exists():
|
||||
instance = InvenTreeSetting.objects.get(key='InstanceName')
|
||||
instance_name = instance.value
|
||||
|
||||
# Delete the legacy key
|
||||
instance.delete()
|
||||
|
||||
# Create new value
|
||||
InvenTreeSetting.objects.create(
|
||||
key='INVENTREE_INSTANCE',
|
||||
value=instance_name
|
||||
)
|
||||
|
||||
except (OperationalError, ProgrammingError, IntegrityError):
|
||||
# Migrations have not yet been applied - table does not exist
|
||||
pass
|
||||
|
||||
def add_default_settings(self):
|
||||
"""
|
||||
Create all required settings, if they do not exist.
|
||||
"""
|
||||
|
||||
from .models import InvenTreeSetting
|
||||
|
||||
for key in InvenTreeSetting.GLOBAL_SETTINGS.keys():
|
||||
try:
|
||||
settings = InvenTreeSetting.objects.filter(key__iexact=key)
|
||||
|
||||
if settings.count() == 0:
|
||||
value = InvenTreeSetting.get_default_value(key)
|
||||
|
||||
print(f"Creating default setting for {key} -> '{value}'")
|
||||
|
||||
InvenTreeSetting.objects.create(
|
||||
key=key,
|
||||
value=value
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
elif settings.count() > 1:
|
||||
# Prevent multiple shadow copies of the same setting!
|
||||
for setting in settings[1:]:
|
||||
setting.delete()
|
||||
|
||||
# Ensure that the key has the correct case
|
||||
setting = settings[0]
|
||||
|
||||
if not setting.key == key:
|
||||
setting.key = key
|
||||
setting.save()
|
||||
|
||||
except (OperationalError, ProgrammingError, IntegrityError):
|
||||
# Table might not yet exist
|
||||
pass
|
||||
pass
|
||||
|
@ -1,16 +0,0 @@
|
||||
# Test fixtures for Currency objects
|
||||
|
||||
- model: common.currency
|
||||
fields:
|
||||
symbol: '$'
|
||||
suffix: 'AUD'
|
||||
description: 'Australian Dollars'
|
||||
base: True
|
||||
|
||||
- model: common.currency
|
||||
fields:
|
||||
symbol: '$'
|
||||
suffix: 'USD'
|
||||
description: 'US Dollars'
|
||||
base: False
|
||||
value: 1.4
|
13
InvenTree/common/fixtures/settings.yaml
Normal file
13
InvenTree/common/fixtures/settings.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
# Sample settings objects
|
||||
|
||||
- model: common.InvenTreeSetting
|
||||
pk: 1
|
||||
fields:
|
||||
key: INVENTREE_INSTANCE
|
||||
value: "My very first InvenTree Instance"
|
||||
|
||||
- model: common.InvenTreeSetting
|
||||
pk: 2
|
||||
fields:
|
||||
key: INVENTREE_COMPANY_NAME
|
||||
value: "ACME Pty Ltd"
|
@ -103,6 +103,13 @@ class InvenTreeSetting(models.Model):
|
||||
'validator': bool
|
||||
},
|
||||
|
||||
'PART_CATEGORY_PARAMETERS': {
|
||||
'name': _('Copy Category Parameter Templates'),
|
||||
'description': _('Copy category parameter templates when creating a part'),
|
||||
'default': True,
|
||||
'validator': bool
|
||||
},
|
||||
|
||||
'PART_COMPONENT': {
|
||||
'name': _('Component'),
|
||||
'description': _('Parts can be used as sub-components by default'),
|
||||
|
@ -12,6 +12,10 @@ class SettingsTest(TestCase):
|
||||
Tests for the 'settings' model
|
||||
"""
|
||||
|
||||
fixtures = [
|
||||
'settings',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
|
||||
User = get_user_model()
|
||||
@ -22,6 +26,20 @@ class SettingsTest(TestCase):
|
||||
|
||||
self.client.login(username='username', password='password')
|
||||
|
||||
def test_settings_objects(self):
|
||||
|
||||
# There should be two settings objects in the database
|
||||
settings = InvenTreeSetting.objects.all()
|
||||
|
||||
self.assertEqual(settings.count(), 2)
|
||||
|
||||
instance_name = InvenTreeSetting.objects.get(pk=1)
|
||||
self.assertEqual(instance_name.key, 'INVENTREE_INSTANCE')
|
||||
self.assertEqual(instance_name.value, 'My very first InvenTree Instance')
|
||||
|
||||
# Check object lookup (case insensitive)
|
||||
self.assertEqual(InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1)
|
||||
|
||||
def test_required_values(self):
|
||||
"""
|
||||
- Ensure that every global setting has a name.
|
||||
|
@ -12,6 +12,7 @@ from .models import PartCategory, Part
|
||||
from .models import PartAttachment, PartStar, PartRelated
|
||||
from .models import BomItem
|
||||
from .models import PartParameterTemplate, PartParameter
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import PartTestTemplate
|
||||
from .models import PartSellPriceBreak
|
||||
|
||||
@ -274,6 +275,11 @@ class ParameterAdmin(ImportExportModelAdmin):
|
||||
list_display = ('part', 'template', 'data')
|
||||
|
||||
|
||||
class PartCategoryParameterAdmin(admin.ModelAdmin):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PartSellPriceBreakAdmin(admin.ModelAdmin):
|
||||
|
||||
class Meta:
|
||||
@ -290,5 +296,6 @@ admin.site.register(PartStar, PartStarAdmin)
|
||||
admin.site.register(BomItem, BomItemAdmin)
|
||||
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
|
||||
admin.site.register(PartParameter, ParameterAdmin)
|
||||
admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin)
|
||||
admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
|
||||
admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)
|
||||
|
@ -21,6 +21,7 @@ from .models import Part, PartCategory, BomItem, PartStar
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
from .models import PartAttachment, PartTestTemplate
|
||||
from .models import PartSellPriceBreak
|
||||
from .models import PartCategoryParameterTemplate
|
||||
|
||||
from build.models import Build
|
||||
|
||||
@ -111,6 +112,36 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
queryset = PartCategory.objects.all()
|
||||
|
||||
|
||||
class CategoryParameters(generics.ListAPIView):
|
||||
""" API endpoint for accessing a list of PartCategory objects.
|
||||
|
||||
- GET: Return a list of PartCategory objects
|
||||
"""
|
||||
|
||||
queryset = PartCategoryParameterTemplate.objects.all()
|
||||
serializer_class = part_serializers.CategoryParameterTemplateSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Custom filtering:
|
||||
- Allow filtering by "null" parent to retrieve top-level part categories
|
||||
"""
|
||||
|
||||
cat_id = self.kwargs.get('pk', None)
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
if cat_id is not None:
|
||||
|
||||
try:
|
||||
cat_id = int(cat_id)
|
||||
queryset = queryset.filter(category=cat_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class PartSalePriceList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for list view of PartSalePriceBreak model
|
||||
@ -864,6 +895,7 @@ part_api_urls = [
|
||||
|
||||
# Base URL for PartCategory API endpoints
|
||||
url(r'^category/', include([
|
||||
url(r'^(?P<pk>\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
|
||||
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
|
||||
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
|
||||
])),
|
||||
|
@ -18,7 +18,7 @@
|
||||
name: Thickness
|
||||
units: mm
|
||||
|
||||
# And some parameters (requires part.yaml)
|
||||
# Add some parameters to parts (requires part.yaml)
|
||||
- model: part.PartParameter
|
||||
pk: 1
|
||||
fields:
|
||||
@ -31,4 +31,19 @@
|
||||
fields:
|
||||
part: 2
|
||||
template: 1
|
||||
data: 12
|
||||
data: 12
|
||||
|
||||
# Add some template parameters to categories (requires category.yaml)
|
||||
- model: part.PartCategoryParameterTemplate
|
||||
pk: 1
|
||||
fields:
|
||||
category: 7
|
||||
parameter_template: 1
|
||||
default_value: '2.8'
|
||||
|
||||
- model: part.PartCategoryParameterTemplate
|
||||
pk: 2
|
||||
fields:
|
||||
category: 7
|
||||
parameter_template: 3
|
||||
default_value: '0.5'
|
||||
|
@ -16,6 +16,7 @@ from django.utils.translation import ugettext as _
|
||||
from .models import Part, PartCategory, PartAttachment, PartRelated
|
||||
from .models import BomItem
|
||||
from .models import PartParameterTemplate, PartParameter
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import PartTestTemplate
|
||||
from .models import PartSellPriceBreak
|
||||
|
||||
@ -199,10 +200,22 @@ class EditPartForm(HelperForm):
|
||||
help_text=_('Confirm part creation'),
|
||||
widget=forms.HiddenInput())
|
||||
|
||||
selected_category_templates = forms.BooleanField(required=False,
|
||||
initial=False,
|
||||
label=_('Include category parameter templates'),
|
||||
widget=forms.HiddenInput())
|
||||
|
||||
parent_category_templates = forms.BooleanField(required=False,
|
||||
initial=False,
|
||||
label=_('Include parent categories parameter templates'),
|
||||
widget=forms.HiddenInput())
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
fields = [
|
||||
'category',
|
||||
'selected_category_templates',
|
||||
'parent_category_templates',
|
||||
'name',
|
||||
'IPN',
|
||||
'description',
|
||||
@ -264,6 +277,28 @@ class EditCategoryForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class EditCategoryParameterTemplateForm(HelperForm):
|
||||
""" Form for editing a PartCategoryParameterTemplate object """
|
||||
|
||||
add_to_same_level_categories = forms.BooleanField(required=False,
|
||||
initial=False,
|
||||
help_text=_('Add parameter template to same level categories'))
|
||||
|
||||
add_to_all_categories = forms.BooleanField(required=False,
|
||||
initial=False,
|
||||
help_text=_('Add parameter template to all categories'))
|
||||
|
||||
class Meta:
|
||||
model = PartCategoryParameterTemplate
|
||||
fields = [
|
||||
'category',
|
||||
'parameter_template',
|
||||
'default_value',
|
||||
'add_to_same_level_categories',
|
||||
'add_to_all_categories',
|
||||
]
|
||||
|
||||
|
||||
class EditBomItemForm(HelperForm):
|
||||
""" Form for editing a BomItem object """
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
# Generated by Django 3.0.7 on 2020-10-27 04:57
|
||||
|
||||
import InvenTree.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0051_bomitem_optional'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='part',
|
||||
name='link',
|
||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True),
|
||||
),
|
||||
]
|
@ -2,7 +2,7 @@
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
import InvenTree.fields
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -19,4 +19,9 @@ class Migration(migrations.Migration):
|
||||
('part_2', models.ForeignKey(help_text='Select Related Part', on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', to='part.Part')),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='part',
|
||||
name='link',
|
||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True),
|
||||
),
|
||||
]
|
||||
|
@ -1,14 +0,0 @@
|
||||
# Generated by Django 3.0.7 on 2020-11-03 10:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0052_auto_20201027_1557'),
|
||||
('part', '0052_partrelated'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.0.7 on 2020-10-30 18:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0052_partrelated'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PartCategoryParameterTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('default_value', models.CharField(blank=True, help_text='Default Parameter Value', max_length=500)),
|
||||
('category', models.ForeignKey(help_text='Part Category', on_delete=django.db.models.deletion.CASCADE, related_name='parameter_templates', to='part.PartCategory')),
|
||||
('parameter_template', models.ForeignKey(help_text='Parameter Template', on_delete=django.db.models.deletion.CASCADE, related_name='part_categories', to='part.PartParameterTemplate')),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='partcategoryparametertemplate',
|
||||
constraint=models.UniqueConstraint(fields=('category', 'parameter_template'), name='unique_category_parameter_template_pair'),
|
||||
),
|
||||
]
|
@ -7,7 +7,7 @@ import part.settings
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0053_merge_20201103_1028'),
|
||||
('part', '0052_partrelated'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
14
InvenTree/part/migrations/0060_merge_20201112_1722.py
Normal file
14
InvenTree/part/migrations/0060_merge_20201112_1722.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Generated by Django 3.0.7 on 2020-11-12 06:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0053_partcategoryparametertemplate'),
|
||||
('part', '0059_auto_20201112_1112'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
@ -12,7 +12,8 @@ from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Sum
|
||||
from django.db.utils import IntegrityError
|
||||
from django.db.models import Sum, UniqueConstraint
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
@ -164,6 +165,26 @@ class PartCategory(InvenTreeTree):
|
||||
|
||||
return category_parameters
|
||||
|
||||
@classmethod
|
||||
def get_parent_categories(cls):
|
||||
""" Return tuple list of parent (root) categories """
|
||||
|
||||
# Get root nodes
|
||||
root_categories = cls.objects.filter(level=0)
|
||||
|
||||
parent_categories = []
|
||||
for category in root_categories:
|
||||
parent_categories.append((category.id, category.name))
|
||||
|
||||
return parent_categories
|
||||
|
||||
def get_parameter_templates(self):
|
||||
""" Return parameter templates associated to category """
|
||||
|
||||
prefetch = PartCategoryParameterTemplate.objects.prefetch_related('category', 'parameter_template')
|
||||
|
||||
return prefetch.filter(category=self.id)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
|
||||
def before_delete_part_category(sender, instance, using, **kwargs):
|
||||
@ -307,6 +328,9 @@ class Part(MPTTModel):
|
||||
If not, it is considered "orphaned" and will be deleted.
|
||||
"""
|
||||
|
||||
# Get category templates settings
|
||||
add_category_templates = kwargs.pop('add_category_templates', None)
|
||||
|
||||
if self.pk:
|
||||
previous = Part.objects.get(pk=self.pk)
|
||||
|
||||
@ -322,6 +346,44 @@ class Part(MPTTModel):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if add_category_templates:
|
||||
# Get part category
|
||||
category = self.category
|
||||
|
||||
if add_category_templates:
|
||||
# Store templates added to part
|
||||
template_list = []
|
||||
|
||||
# Create part parameters for selected category
|
||||
category_templates = add_category_templates['main']
|
||||
if category_templates:
|
||||
for template in category.get_parameter_templates():
|
||||
parameter = PartParameter.create(part=self,
|
||||
template=template.parameter_template,
|
||||
data=template.default_value,
|
||||
save=True)
|
||||
if parameter:
|
||||
template_list.append(template.parameter_template)
|
||||
|
||||
# Create part parameters for parent category
|
||||
category_templates = add_category_templates['parent']
|
||||
if category_templates:
|
||||
# Get parent categories
|
||||
parent_categories = category.get_ancestors()
|
||||
|
||||
for category in parent_categories:
|
||||
for template in category.get_parameter_templates():
|
||||
# Check that template wasn't already added
|
||||
if template.parameter_template not in template_list:
|
||||
try:
|
||||
PartParameter.create(part=self,
|
||||
template=template.parameter_template,
|
||||
data=template.default_value,
|
||||
save=True)
|
||||
except IntegrityError:
|
||||
# PartParameter already exists
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.full_name} - {self.description}"
|
||||
|
||||
@ -571,7 +633,8 @@ class Part(MPTTModel):
|
||||
super().clean()
|
||||
|
||||
if self.trackable:
|
||||
for parent_part in self.used_in.all():
|
||||
for item in self.used_in.all():
|
||||
parent_part = item.part
|
||||
if not parent_part.trackable:
|
||||
parent_part.trackable = True
|
||||
parent_part.clean()
|
||||
@ -1041,8 +1104,16 @@ class Part(MPTTModel):
|
||||
- Exclude parts which this part is in the BOM for
|
||||
"""
|
||||
|
||||
parts = Part.objects.filter(component=True).exclude(id=self.id)
|
||||
parts = parts.exclude(id__in=[part.id for part in self.used_in.all()])
|
||||
# Start with a list of all parts designated as 'sub components'
|
||||
parts = Part.objects.filter(component=True)
|
||||
|
||||
# Exclude this part
|
||||
parts = parts.exclude(id=self.id)
|
||||
|
||||
# Exclude any parts that this part is used *in* (to prevent recursive BOMs)
|
||||
used_in = self.used_in.all()
|
||||
|
||||
parts = parts.exclude(id__in=[item.part.id for item in used_in])
|
||||
|
||||
return parts
|
||||
|
||||
@ -1655,6 +1726,49 @@ class PartParameter(models.Model):
|
||||
return part_parameter
|
||||
|
||||
|
||||
class PartCategoryParameterTemplate(models.Model):
|
||||
"""
|
||||
A PartCategoryParameterTemplate creates a unique relationship between a PartCategory
|
||||
and a PartParameterTemplate.
|
||||
Multiple PartParameterTemplate instances can be associated to a PartCategory to drive
|
||||
a default list of parameter templates attached to a Part instance upon creation.
|
||||
|
||||
Attributes:
|
||||
category: Reference to a single PartCategory object
|
||||
parameter_template: Reference to a single PartParameterTemplate object
|
||||
default_value: The default value for the parameter in the context of the selected
|
||||
category
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
UniqueConstraint(fields=['category', 'parameter_template'],
|
||||
name='unique_category_parameter_template_pair')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
""" String representation of a PartCategoryParameterTemplate (admin interface) """
|
||||
|
||||
if self.default_value:
|
||||
return f'{self.category.name} | {self.parameter_template.name} | {self.default_value}'
|
||||
else:
|
||||
return f'{self.category.name} | {self.parameter_template.name}'
|
||||
|
||||
category = models.ForeignKey(PartCategory,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='parameter_templates',
|
||||
help_text=_('Part Category'))
|
||||
|
||||
parameter_template = models.ForeignKey(PartParameterTemplate,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='part_categories',
|
||||
help_text=_('Parameter Template'))
|
||||
|
||||
default_value = models.CharField(max_length=500,
|
||||
blank=True,
|
||||
help_text=_('Default Parameter Value'))
|
||||
|
||||
|
||||
class BomItem(models.Model):
|
||||
""" A BomItem links a part to its component items.
|
||||
A part can have a BOM (bill of materials) which defines
|
||||
|
@ -15,7 +15,7 @@ from stock.models import StockItem
|
||||
|
||||
from .models import (BomItem, Part, PartAttachment, PartCategory,
|
||||
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
||||
PartStar, PartTestTemplate)
|
||||
PartStar, PartTestTemplate, PartCategoryParameterTemplate)
|
||||
|
||||
|
||||
class CategorySerializer(InvenTreeModelSerializer):
|
||||
@ -418,3 +418,21 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
|
||||
'name',
|
||||
'units',
|
||||
]
|
||||
|
||||
|
||||
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for PartCategoryParameterTemplate """
|
||||
|
||||
parameter_template_detail = PartParameterTemplateSerializer(source='parameter_template',
|
||||
many=False,
|
||||
read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PartCategoryParameterTemplate
|
||||
fields = [
|
||||
'pk',
|
||||
'category',
|
||||
'parameter_template',
|
||||
'parameter_template_detail',
|
||||
'default_value',
|
||||
]
|
||||
|
@ -3,10 +3,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
import django.core.exceptions as django_exceptions
|
||||
|
||||
from .models import Part, PartCategory
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
from .models import PartCategoryParameterTemplate
|
||||
|
||||
|
||||
class TestParams(TestCase):
|
||||
@ -24,7 +26,10 @@ class TestParams(TestCase):
|
||||
self.assertEquals(str(t1), 'Length (mm)')
|
||||
|
||||
p1 = PartParameter.objects.get(pk=1)
|
||||
self.assertEqual(str(p1), "M2x4 LPHS : Length = 4mm")
|
||||
self.assertEqual(str(p1), 'M2x4 LPHS : Length = 4mm')
|
||||
|
||||
c1 = PartCategoryParameterTemplate.objects.get(pk=1)
|
||||
self.assertEqual(str(c1), 'Mechanical | Length | 2.8')
|
||||
|
||||
def test_validate(self):
|
||||
|
||||
@ -40,3 +45,47 @@ class TestParams(TestCase):
|
||||
t3 = PartParameterTemplate(name='aBcde', units='dd')
|
||||
t3.full_clean()
|
||||
t3.save()
|
||||
|
||||
|
||||
class TestCategoryTemplates(TransactionTestCase):
|
||||
|
||||
fixtures = [
|
||||
'location',
|
||||
'category',
|
||||
'part',
|
||||
'params'
|
||||
]
|
||||
|
||||
def test_validate(self):
|
||||
|
||||
# Category templates
|
||||
n = PartCategoryParameterTemplate.objects.all().count()
|
||||
self.assertEqual(n, 2)
|
||||
|
||||
category = PartCategory.objects.get(pk=8)
|
||||
|
||||
t1 = PartParameterTemplate.objects.get(pk=2)
|
||||
c1 = PartCategoryParameterTemplate(category=category,
|
||||
parameter_template=t1,
|
||||
default_value='xyz')
|
||||
c1.save()
|
||||
|
||||
n = PartCategoryParameterTemplate.objects.all().count()
|
||||
self.assertEqual(n, 3)
|
||||
|
||||
# Get test part
|
||||
part = Part.objects.get(pk=1)
|
||||
|
||||
# Get part parameters count
|
||||
n_param = part.get_parameters().count()
|
||||
|
||||
add_category_templates = {
|
||||
'main': True,
|
||||
'parent': True,
|
||||
}
|
||||
# Save it with category parameters
|
||||
part.save(**{'add_category_templates': add_category_templates})
|
||||
|
||||
# Check new part parameters count
|
||||
# Only 2 parameters should be added as one already existed with same template
|
||||
self.assertEqual(n_param + 2, part.get_parameters().count())
|
||||
|
@ -80,10 +80,18 @@ part_detail_urls = [
|
||||
url(r'^.*$', views.PartDetail.as_view(), name='part-detail'),
|
||||
]
|
||||
|
||||
category_parameter_urls = [
|
||||
url(r'^new/', views.CategoryParameterTemplateCreate.as_view(), name='category-param-template-create'),
|
||||
url(r'^(?P<pid>\d+)/edit/', views.CategoryParameterTemplateEdit.as_view(), name='category-param-template-edit'),
|
||||
url(r'^(?P<pid>\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'),
|
||||
]
|
||||
|
||||
part_category_urls = [
|
||||
url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
|
||||
url(r'^parameters/', include(category_parameter_urls)),
|
||||
|
||||
url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
]
|
||||
|
@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import HttpResponseRedirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -25,6 +26,7 @@ from decimal import Decimal, InvalidOperation
|
||||
|
||||
from .models import PartCategory, Part, PartAttachment, PartRelated
|
||||
from .models import PartParameterTemplate, PartParameter
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import BomItem
|
||||
from .models import match_part_names
|
||||
from .models import PartTestTemplate
|
||||
@ -627,6 +629,10 @@ class PartCreate(AjaxCreateView):
|
||||
# Hide the default_supplier field (there are no matching supplier parts yet!)
|
||||
form.fields['default_supplier'].widget = HiddenInput()
|
||||
|
||||
# Display category templates widgets
|
||||
form.fields['selected_category_templates'].widget = CheckboxInput()
|
||||
form.fields['parent_category_templates'].widget = CheckboxInput()
|
||||
|
||||
return form
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -669,7 +675,14 @@ class PartCreate(AjaxCreateView):
|
||||
# Record the user who created this part
|
||||
part.creation_user = request.user
|
||||
|
||||
part.save()
|
||||
# Store category templates settings
|
||||
add_category_templates = {
|
||||
'main': form.cleaned_data['selected_category_templates'],
|
||||
'parent': form.cleaned_data['parent_category_templates'],
|
||||
}
|
||||
|
||||
# Save part and pass category template settings
|
||||
part.save(**{'add_category_templates': add_category_templates})
|
||||
|
||||
data['pk'] = part.pk
|
||||
data['text'] = str(part)
|
||||
@ -702,6 +715,10 @@ class PartCreate(AjaxCreateView):
|
||||
if label in self.request.GET:
|
||||
initials[label] = self.request.GET.get(label)
|
||||
|
||||
# Automatically create part parameters from category templates
|
||||
initials['selected_category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False))
|
||||
initials['parent_category_templates'] = initials['selected_category_templates']
|
||||
|
||||
return initials
|
||||
|
||||
|
||||
@ -2205,6 +2222,185 @@ class CategoryCreate(AjaxCreateView):
|
||||
return initials
|
||||
|
||||
|
||||
class CategoryParameterTemplateCreate(AjaxCreateView):
|
||||
""" View for creating a new PartCategoryParameterTemplate """
|
||||
|
||||
role_required = 'part.add'
|
||||
|
||||
model = PartCategoryParameterTemplate
|
||||
form_class = part_forms.EditCategoryParameterTemplateForm
|
||||
ajax_form_title = _('Create Category Parameter Template')
|
||||
|
||||
def get_initial(self):
|
||||
""" Get initial data for Category """
|
||||
initials = super().get_initial()
|
||||
|
||||
category_id = self.kwargs.get('pk', None)
|
||||
|
||||
if category_id:
|
||||
try:
|
||||
initials['category'] = PartCategory.objects.get(pk=category_id)
|
||||
except (PartCategory.DoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
def get_form(self):
|
||||
""" Create a form to upload a new CategoryParameterTemplate
|
||||
- Hide the 'category' field (parent part)
|
||||
- Display parameter templates which are not yet related
|
||||
"""
|
||||
|
||||
form = super(AjaxCreateView, self).get_form()
|
||||
|
||||
form.fields['category'].widget = HiddenInput()
|
||||
|
||||
if form.is_valid():
|
||||
form.cleaned_data['category'] = self.kwargs.get('pk', None)
|
||||
|
||||
try:
|
||||
# Get selected category
|
||||
category = self.get_initial()['category']
|
||||
|
||||
# Get existing parameter templates
|
||||
parameters = [template.parameter_template.pk
|
||||
for template in category.get_parameter_templates()]
|
||||
|
||||
# Exclude templates already linked to category
|
||||
updated_choices = []
|
||||
for choice in form.fields["parameter_template"].choices:
|
||||
if (choice[0] not in parameters):
|
||||
updated_choices.append(choice)
|
||||
|
||||
# Update choices for parameter templates
|
||||
form.fields['parameter_template'].choices = updated_choices
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return form
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Capture the POST request
|
||||
|
||||
- If the add_to_all_categories object is set, link parameter template to
|
||||
all categories
|
||||
- If the add_to_same_level_categories object is set, link parameter template to
|
||||
same level categories
|
||||
"""
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
valid = form.is_valid()
|
||||
|
||||
if valid:
|
||||
add_to_same_level_categories = form.cleaned_data['add_to_same_level_categories']
|
||||
add_to_all_categories = form.cleaned_data['add_to_all_categories']
|
||||
|
||||
selected_category = PartCategory.objects.get(pk=int(self.kwargs['pk']))
|
||||
parameter_template = form.cleaned_data['parameter_template']
|
||||
default_value = form.cleaned_data['default_value']
|
||||
|
||||
categories = PartCategory.objects.all()
|
||||
|
||||
if add_to_same_level_categories and not add_to_all_categories:
|
||||
# Get level
|
||||
level = selected_category.level
|
||||
# Filter same level categories
|
||||
categories = categories.filter(level=level)
|
||||
|
||||
if add_to_same_level_categories or add_to_all_categories:
|
||||
# Add parameter template and default value to categories
|
||||
for category in categories:
|
||||
# Skip selected category (will be processed in the post call)
|
||||
if category.pk != selected_category.pk:
|
||||
try:
|
||||
cat_template = PartCategoryParameterTemplate.objects.create(category=category,
|
||||
parameter_template=parameter_template,
|
||||
default_value=default_value)
|
||||
cat_template.save()
|
||||
except IntegrityError:
|
||||
# Parameter template is already linked to category
|
||||
pass
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class CategoryParameterTemplateEdit(AjaxUpdateView):
|
||||
""" View for editing a PartCategoryParameterTemplate """
|
||||
|
||||
role_required = 'part.change'
|
||||
|
||||
model = PartCategoryParameterTemplate
|
||||
form_class = part_forms.EditCategoryParameterTemplateForm
|
||||
ajax_form_title = _('Edit Category Parameter Template')
|
||||
|
||||
def get_object(self):
|
||||
try:
|
||||
self.object = self.model.objects.get(pk=self.kwargs['pid'])
|
||||
except:
|
||||
return None
|
||||
|
||||
return self.object
|
||||
|
||||
def get_form(self):
|
||||
""" Create a form to upload a new CategoryParameterTemplate
|
||||
- Hide the 'category' field (parent part)
|
||||
- Display parameter templates which are not yet related
|
||||
"""
|
||||
|
||||
form = super(AjaxUpdateView, self).get_form()
|
||||
|
||||
form.fields['category'].widget = HiddenInput()
|
||||
form.fields['add_to_all_categories'].widget = HiddenInput()
|
||||
form.fields['add_to_same_level_categories'].widget = HiddenInput()
|
||||
|
||||
if form.is_valid():
|
||||
form.cleaned_data['category'] = self.kwargs.get('pk', None)
|
||||
|
||||
try:
|
||||
# Get selected category
|
||||
category = PartCategory.objects.get(pk=self.kwargs.get('pk', None))
|
||||
# Get selected template
|
||||
selected_template = self.get_object().parameter_template
|
||||
|
||||
# Get existing parameter templates
|
||||
parameters = [template.parameter_template.pk
|
||||
for template in category.get_parameter_templates()
|
||||
if template.parameter_template.pk != selected_template.pk]
|
||||
|
||||
# Exclude templates already linked to category
|
||||
updated_choices = []
|
||||
for choice in form.fields["parameter_template"].choices:
|
||||
if (choice[0] not in parameters):
|
||||
updated_choices.append(choice)
|
||||
|
||||
# Update choices for parameter templates
|
||||
form.fields['parameter_template'].choices = updated_choices
|
||||
# Set initial choice to current template
|
||||
form.fields['parameter_template'].initial = selected_template
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class CategoryParameterTemplateDelete(AjaxDeleteView):
|
||||
""" View for deleting an existing PartCategoryParameterTemplate """
|
||||
|
||||
role_required = 'part.delete'
|
||||
|
||||
model = PartCategoryParameterTemplate
|
||||
ajax_form_title = _("Delete Category Parameter Template")
|
||||
|
||||
def get_object(self):
|
||||
try:
|
||||
self.object = self.model.objects.get(pk=self.kwargs['pid'])
|
||||
except:
|
||||
return None
|
||||
|
||||
return self.object
|
||||
|
||||
|
||||
class BomItemDetail(InvenTreeRoleMixin, DetailView):
|
||||
""" Detail view for BomItem """
|
||||
context_object_name = 'item'
|
||||
|
114
InvenTree/templates/InvenTree/settings/category.html
Normal file
114
InvenTree/templates/InvenTree/settings/category.html
Normal file
@ -0,0 +1,114 @@
|
||||
{% extends "InvenTree/settings/settings.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include "InvenTree/settings/tabs.html" with tab='category' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{% trans "Category Settings" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block settings %}
|
||||
|
||||
<form action="{% url 'settings-category' %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
<div id="category-select">
|
||||
{% crispy form %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if category %}
|
||||
<hr>
|
||||
|
||||
<h4>{% trans "Category Parameter Templates" %}</h4>
|
||||
|
||||
<div id='param-buttons'>
|
||||
<button class='btn btn-success' id='new-param'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='param-table' data-toolbar='#param-buttons'>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{# Convert dropdown to select2 format #}
|
||||
$(document).ready(function() {
|
||||
attachSelect('#category-select');
|
||||
});
|
||||
|
||||
{% if category %}
|
||||
$("#param-table").inventreeTable({
|
||||
url: "{% url 'api-part-category-parameters' category.pk %}",
|
||||
queryParams: {
|
||||
ordering: 'name',
|
||||
},
|
||||
formatNoMatches: function() { return '{% trans "No category parameter templates found" %}'; },
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'parameter_template_detail.name',
|
||||
title: '{% trans "Parameter Template" %}',
|
||||
sortable: 'true',
|
||||
},
|
||||
{
|
||||
field: 'default_value',
|
||||
title: '{% trans "Default Value" %}',
|
||||
sortable: 'true',
|
||||
formatter: function(value, row, index, field) {
|
||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||
|
||||
var html = value
|
||||
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#new-param").click(function() {
|
||||
launchModalForm("{% url 'category-param-template-create' category.pk %}", {
|
||||
success: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$("#param-table").on('click', '.template-edit', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/edit/";
|
||||
|
||||
launchModalForm(url, {
|
||||
success: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#param-table").on('click', '.template-delete', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/delete/";
|
||||
|
||||
launchModalForm(url, {
|
||||
success: function() {
|
||||
$("#param-table").bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -27,6 +27,7 @@
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_COPY_TESTS" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="PART_CATEGORY_PARAMETERS" %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@ -15,6 +15,9 @@
|
||||
<li {% if tab == 'global' %} class='active' {% endif %}>
|
||||
<a href='{% url "settings-global" %}'><span class='fas fa-globe'></span> {% trans "Global" %}</a>
|
||||
</li>
|
||||
<li{% ifequal tab 'category' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-category' %}"><span class='fa fa-sitemap'></span> {% trans "Categories" %}</a>
|
||||
</li>
|
||||
<li{% ifequal tab 'part' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> {% trans "Parts" %}</a>
|
||||
</li>
|
||||
@ -31,4 +34,4 @@
|
||||
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -58,6 +58,7 @@ class RuleSet(models.Model):
|
||||
'part_partparametertemplate',
|
||||
'part_partparameter',
|
||||
'part_partrelated',
|
||||
'part_partcategoryparametertemplate',
|
||||
],
|
||||
'stock': [
|
||||
'stock_stockitem',
|
||||
@ -108,6 +109,9 @@ class RuleSet(models.Model):
|
||||
'report_reportasset',
|
||||
'report_testreport',
|
||||
'part_partstar',
|
||||
|
||||
# Third-party tables
|
||||
'error_report_error',
|
||||
'exchange_rate',
|
||||
'exchange_exchangebackend',
|
||||
]
|
||||
|
@ -28,5 +28,6 @@ django-debug-toolbar==2.2 # Debug / profiling toolbar
|
||||
django-admin-shell==0.1.2 # Python shell for the admin interface
|
||||
django-money==1.1 # Django app for currency management
|
||||
certifi # Certifi is (most likely) installed through one of the requirements above
|
||||
django-error-report==0.2.0 # Error report viewer for the admin interface
|
||||
|
||||
inventree # Install the latest version of the InvenTree API python library
|
||||
|
91
tasks.py
91
tasks.py
@ -6,6 +6,7 @@ from shutil import copyfile
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
import sys
|
||||
|
||||
def apps():
|
||||
"""
|
||||
@ -238,6 +239,96 @@ def postgresql(c):
|
||||
c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev')
|
||||
c.run('pip3 install psycopg2')
|
||||
|
||||
@task(help={'filename': "Output filename (default = 'data.json')"})
|
||||
def export_records(c, filename='data.json'):
|
||||
"""
|
||||
Export all database records to a file
|
||||
"""
|
||||
|
||||
# Get an absolute path to the file
|
||||
if not os.path.isabs(filename):
|
||||
filename = os.path.join(localDir(), filename)
|
||||
filename = os.path.abspath(filename)
|
||||
|
||||
print(f"Exporting database records to file '{filename}'")
|
||||
|
||||
if os.path.exists(filename):
|
||||
response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ")
|
||||
response = str(response).strip().lower()
|
||||
|
||||
if response not in ['y', 'yes']:
|
||||
print("Cancelled export operation")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = f'dumpdata --exclude contenttypes --exclude auth.permission --indent 2 --output {filename}'
|
||||
|
||||
manage(c, cmd, pty=True)
|
||||
|
||||
@task(help={'filename': 'Input filename'})
|
||||
def import_records(c, filename='data.json'):
|
||||
"""
|
||||
Import database records from a file
|
||||
"""
|
||||
|
||||
# Get an absolute path to the supplied filename
|
||||
if not os.path.isabs(filename):
|
||||
filename = os.path.join(localDir(), filename)
|
||||
|
||||
if not os.path.exists(filename):
|
||||
print(f"Error: File '{filename}' does not exist")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Importing database records from '{filename}'")
|
||||
|
||||
cmd = f'loaddata {filename}'
|
||||
|
||||
manage(c, cmd, pty=True)
|
||||
|
||||
@task
|
||||
def import_fixtures(c):
|
||||
"""
|
||||
Import fixture data into the database.
|
||||
|
||||
This command imports all existing test fixture data into the database.
|
||||
|
||||
Warning:
|
||||
- Intended for testing / development only!
|
||||
- Running this command may overwrite existing database data!!
|
||||
- Don't say you were not warned...
|
||||
"""
|
||||
|
||||
fixtures = [
|
||||
# Build model
|
||||
'build',
|
||||
|
||||
# Common models
|
||||
'settings',
|
||||
|
||||
# Company model
|
||||
'company',
|
||||
'price_breaks',
|
||||
'supplier_part',
|
||||
|
||||
# Order model
|
||||
'order',
|
||||
|
||||
# Part model
|
||||
'bom',
|
||||
'category',
|
||||
'params',
|
||||
'part',
|
||||
'test_templates',
|
||||
|
||||
# Stock model
|
||||
'location',
|
||||
'stock_tests',
|
||||
'stock',
|
||||
]
|
||||
|
||||
command = 'loaddata ' + ' '.join(fixtures)
|
||||
|
||||
manage(c, command, pty=True)
|
||||
|
||||
@task
|
||||
def backup(c):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user