mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +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:
|
script:
|
||||||
- cd InvenTree && python3 manage.py makemigrations && cd ..
|
- cd InvenTree && python3 manage.py makemigrations && cd ..
|
||||||
- python3 ci/check_migration_files.py
|
- python3 ci/check_migration_files.py
|
||||||
|
# Run unit testing / code coverage tests
|
||||||
- invoke coverage
|
- invoke coverage
|
||||||
|
# Run unit test for SQL database backend
|
||||||
- cd InvenTree && python3 manage.py test --settings=InvenTree.ci_mysql && cd ..
|
- 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 ..
|
- cd InvenTree && python3 manage.py test --settings=InvenTree.ci_postgresql && cd ..
|
||||||
- invoke translate
|
- invoke translate
|
||||||
- invoke style
|
- 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:
|
after_success:
|
||||||
- coveralls
|
- coveralls
|
@ -12,6 +12,7 @@ from crispy_forms.layout import Layout, Field
|
|||||||
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
|
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from common.models import ColorTheme
|
from common.models import ColorTheme
|
||||||
|
from part.models import PartCategory
|
||||||
|
|
||||||
|
|
||||||
class HelperForm(forms.ModelForm):
|
class HelperForm(forms.ModelForm):
|
||||||
@ -200,3 +201,33 @@ class ColorThemeSelectForm(forms.ModelForm):
|
|||||||
css_class='row',
|
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
|
'django_admin_shell', # Python shell for the admin interface
|
||||||
'djmoney', # django-money integration
|
'djmoney', # django-money integration
|
||||||
'djmoney.contrib.exchange', # django-money exchange rates
|
'djmoney.contrib.exchange', # django-money exchange rates
|
||||||
|
'error_report', # Error reporting in the admin interface
|
||||||
]
|
]
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
@ -183,6 +184,9 @@ MIDDLEWARE = CONFIG.get('middleware', [
|
|||||||
'InvenTree.middleware.AuthRequiredMiddleware'
|
'InvenTree.middleware.AuthRequiredMiddleware'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Error reporting middleware
|
||||||
|
MIDDLEWARE.append('error_report.middleware.ExceptionProcessor')
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
|
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
|
||||||
'django.contrib.auth.backends.ModelBackend'
|
'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 rest_framework.documentation import include_docs_urls
|
||||||
|
|
||||||
from .views import IndexView, SearchView, DatabaseStatsView
|
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 .views import DynamicJsView
|
||||||
|
|
||||||
from common.views import SettingEdit
|
from common.views import SettingEdit
|
||||||
@ -74,6 +75,7 @@ settings_urls = [
|
|||||||
url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
|
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'^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'^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'^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'),
|
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'^edit-user/', EditUserView.as_view(), name='edit-user'),
|
||||||
url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
|
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/shell/', include('django_admin_shell.urls')),
|
||||||
url(r'^admin/', admin.site.urls, name='inventree-admin'),
|
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 common.models import InvenTreeSetting, ColorTheme
|
||||||
from users.models import check_user_role, RuleSet
|
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 .helpers import str2bool
|
||||||
|
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
@ -750,6 +751,42 @@ class ColorThemeSelectView(FormView):
|
|||||||
return self.form_invalid(form)
|
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):
|
class DatabaseStatsView(AjaxView):
|
||||||
""" View for displaying database statistics """
|
""" View for displaying database statistics """
|
||||||
|
|
||||||
|
@ -1,92 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.utils import OperationalError, ProgrammingError, IntegrityError
|
|
||||||
|
|
||||||
|
|
||||||
class CommonConfig(AppConfig):
|
class CommonConfig(AppConfig):
|
||||||
name = 'common'
|
name = 'common'
|
||||||
|
|
||||||
def ready(self):
|
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
|
'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': {
|
'PART_COMPONENT': {
|
||||||
'name': _('Component'),
|
'name': _('Component'),
|
||||||
'description': _('Parts can be used as sub-components by default'),
|
'description': _('Parts can be used as sub-components by default'),
|
||||||
|
@ -12,6 +12,10 @@ class SettingsTest(TestCase):
|
|||||||
Tests for the 'settings' model
|
Tests for the 'settings' model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'settings',
|
||||||
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -22,6 +26,20 @@ class SettingsTest(TestCase):
|
|||||||
|
|
||||||
self.client.login(username='username', password='password')
|
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):
|
def test_required_values(self):
|
||||||
"""
|
"""
|
||||||
- Ensure that every global setting has a name.
|
- 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 PartAttachment, PartStar, PartRelated
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import PartTestTemplate
|
from .models import PartTestTemplate
|
||||||
from .models import PartSellPriceBreak
|
from .models import PartSellPriceBreak
|
||||||
|
|
||||||
@ -274,6 +275,11 @@ class ParameterAdmin(ImportExportModelAdmin):
|
|||||||
list_display = ('part', 'template', 'data')
|
list_display = ('part', 'template', 'data')
|
||||||
|
|
||||||
|
|
||||||
|
class PartCategoryParameterAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PartSellPriceBreakAdmin(admin.ModelAdmin):
|
class PartSellPriceBreakAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -290,5 +296,6 @@ admin.site.register(PartStar, PartStarAdmin)
|
|||||||
admin.site.register(BomItem, BomItemAdmin)
|
admin.site.register(BomItem, BomItemAdmin)
|
||||||
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
|
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
|
||||||
admin.site.register(PartParameter, ParameterAdmin)
|
admin.site.register(PartParameter, ParameterAdmin)
|
||||||
|
admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin)
|
||||||
admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
|
admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
|
||||||
admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)
|
admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)
|
||||||
|
@ -21,6 +21,7 @@ from .models import Part, PartCategory, BomItem, PartStar
|
|||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
from .models import PartAttachment, PartTestTemplate
|
from .models import PartAttachment, PartTestTemplate
|
||||||
from .models import PartSellPriceBreak
|
from .models import PartSellPriceBreak
|
||||||
|
from .models import PartCategoryParameterTemplate
|
||||||
|
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
|
|
||||||
@ -111,6 +112,36 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
queryset = PartCategory.objects.all()
|
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):
|
class PartSalePriceList(generics.ListCreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for list view of PartSalePriceBreak model
|
API endpoint for list view of PartSalePriceBreak model
|
||||||
@ -864,6 +895,7 @@ part_api_urls = [
|
|||||||
|
|
||||||
# Base URL for PartCategory API endpoints
|
# Base URL for PartCategory API endpoints
|
||||||
url(r'^category/', include([
|
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'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
|
||||||
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
|
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
|
||||||
])),
|
])),
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
name: Thickness
|
name: Thickness
|
||||||
units: mm
|
units: mm
|
||||||
|
|
||||||
# And some parameters (requires part.yaml)
|
# Add some parameters to parts (requires part.yaml)
|
||||||
- model: part.PartParameter
|
- model: part.PartParameter
|
||||||
pk: 1
|
pk: 1
|
||||||
fields:
|
fields:
|
||||||
@ -32,3 +32,18 @@
|
|||||||
part: 2
|
part: 2
|
||||||
template: 1
|
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 Part, PartCategory, PartAttachment, PartRelated
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import PartTestTemplate
|
from .models import PartTestTemplate
|
||||||
from .models import PartSellPriceBreak
|
from .models import PartSellPriceBreak
|
||||||
|
|
||||||
@ -199,10 +200,22 @@ class EditPartForm(HelperForm):
|
|||||||
help_text=_('Confirm part creation'),
|
help_text=_('Confirm part creation'),
|
||||||
widget=forms.HiddenInput())
|
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:
|
class Meta:
|
||||||
model = Part
|
model = Part
|
||||||
fields = [
|
fields = [
|
||||||
'category',
|
'category',
|
||||||
|
'selected_category_templates',
|
||||||
|
'parent_category_templates',
|
||||||
'name',
|
'name',
|
||||||
'IPN',
|
'IPN',
|
||||||
'description',
|
'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):
|
class EditBomItemForm(HelperForm):
|
||||||
""" Form for editing a BomItem object """
|
""" 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
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import InvenTree.fields
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
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')),
|
('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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('part', '0053_merge_20201103_1028'),
|
('part', '0052_partrelated'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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.urls import reverse
|
||||||
|
|
||||||
from django.db import models, transaction
|
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.db.models.functions import Coalesce
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
@ -164,6 +165,26 @@ class PartCategory(InvenTreeTree):
|
|||||||
|
|
||||||
return category_parameters
|
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')
|
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
|
||||||
def before_delete_part_category(sender, instance, using, **kwargs):
|
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.
|
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:
|
if self.pk:
|
||||||
previous = Part.objects.get(pk=self.pk)
|
previous = Part.objects.get(pk=self.pk)
|
||||||
|
|
||||||
@ -322,6 +346,44 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
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):
|
def __str__(self):
|
||||||
return f"{self.full_name} - {self.description}"
|
return f"{self.full_name} - {self.description}"
|
||||||
|
|
||||||
@ -571,7 +633,8 @@ class Part(MPTTModel):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.trackable:
|
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:
|
if not parent_part.trackable:
|
||||||
parent_part.trackable = True
|
parent_part.trackable = True
|
||||||
parent_part.clean()
|
parent_part.clean()
|
||||||
@ -1041,8 +1104,16 @@ class Part(MPTTModel):
|
|||||||
- Exclude parts which this part is in the BOM for
|
- Exclude parts which this part is in the BOM for
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parts = Part.objects.filter(component=True).exclude(id=self.id)
|
# Start with a list of all parts designated as 'sub components'
|
||||||
parts = parts.exclude(id__in=[part.id for part in self.used_in.all()])
|
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
|
return parts
|
||||||
|
|
||||||
@ -1655,6 +1726,49 @@ class PartParameter(models.Model):
|
|||||||
return part_parameter
|
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):
|
class BomItem(models.Model):
|
||||||
""" A BomItem links a part to its component items.
|
""" A BomItem links a part to its component items.
|
||||||
A part can have a BOM (bill of materials) which defines
|
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,
|
from .models import (BomItem, Part, PartAttachment, PartCategory,
|
||||||
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
||||||
PartStar, PartTestTemplate)
|
PartStar, PartTestTemplate, PartCategoryParameterTemplate)
|
||||||
|
|
||||||
|
|
||||||
class CategorySerializer(InvenTreeModelSerializer):
|
class CategorySerializer(InvenTreeModelSerializer):
|
||||||
@ -418,3 +418,21 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
|
|||||||
'name',
|
'name',
|
||||||
'units',
|
'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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase, TransactionTestCase
|
||||||
import django.core.exceptions as django_exceptions
|
import django.core.exceptions as django_exceptions
|
||||||
|
|
||||||
|
from .models import Part, PartCategory
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
|
from .models import PartCategoryParameterTemplate
|
||||||
|
|
||||||
|
|
||||||
class TestParams(TestCase):
|
class TestParams(TestCase):
|
||||||
@ -24,7 +26,10 @@ class TestParams(TestCase):
|
|||||||
self.assertEquals(str(t1), 'Length (mm)')
|
self.assertEquals(str(t1), 'Length (mm)')
|
||||||
|
|
||||||
p1 = PartParameter.objects.get(pk=1)
|
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):
|
def test_validate(self):
|
||||||
|
|
||||||
@ -40,3 +45,47 @@ class TestParams(TestCase):
|
|||||||
t3 = PartParameterTemplate(name='aBcde', units='dd')
|
t3 = PartParameterTemplate(name='aBcde', units='dd')
|
||||||
t3.full_clean()
|
t3.full_clean()
|
||||||
t3.save()
|
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'),
|
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 = [
|
part_category_urls = [
|
||||||
url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
||||||
url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
|
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'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
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.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.shortcuts import HttpResponseRedirect
|
from django.shortcuts import HttpResponseRedirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 PartCategory, Part, PartAttachment, PartRelated
|
||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import match_part_names
|
from .models import match_part_names
|
||||||
from .models import PartTestTemplate
|
from .models import PartTestTemplate
|
||||||
@ -627,6 +629,10 @@ class PartCreate(AjaxCreateView):
|
|||||||
# Hide the default_supplier field (there are no matching supplier parts yet!)
|
# Hide the default_supplier field (there are no matching supplier parts yet!)
|
||||||
form.fields['default_supplier'].widget = HiddenInput()
|
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
|
return form
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@ -669,7 +675,14 @@ class PartCreate(AjaxCreateView):
|
|||||||
# Record the user who created this part
|
# Record the user who created this part
|
||||||
part.creation_user = request.user
|
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['pk'] = part.pk
|
||||||
data['text'] = str(part)
|
data['text'] = str(part)
|
||||||
@ -702,6 +715,10 @@ class PartCreate(AjaxCreateView):
|
|||||||
if label in self.request.GET:
|
if label in self.request.GET:
|
||||||
initials[label] = self.request.GET.get(label)
|
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
|
return initials
|
||||||
|
|
||||||
|
|
||||||
@ -2205,6 +2222,185 @@ class CategoryCreate(AjaxCreateView):
|
|||||||
return initials
|
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):
|
class BomItemDetail(InvenTreeRoleMixin, DetailView):
|
||||||
""" Detail view for BomItem """
|
""" Detail view for BomItem """
|
||||||
context_object_name = 'item'
|
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_BOM" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %}
|
{% 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_COPY_TESTS" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="PART_CATEGORY_PARAMETERS" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
<li {% if tab == 'global' %} class='active' {% endif %}>
|
<li {% if tab == 'global' %} class='active' {% endif %}>
|
||||||
<a href='{% url "settings-global" %}'><span class='fas fa-globe'></span> {% trans "Global" %}</a>
|
<a href='{% url "settings-global" %}'><span class='fas fa-globe'></span> {% trans "Global" %}</a>
|
||||||
</li>
|
</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 %}>
|
<li{% ifequal tab 'part' %} class='active'{% endifequal %}>
|
||||||
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> {% trans "Parts" %}</a>
|
<a href="{% url 'settings-part' %}"><span class='fas fa-shapes'></span> {% trans "Parts" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -58,6 +58,7 @@ class RuleSet(models.Model):
|
|||||||
'part_partparametertemplate',
|
'part_partparametertemplate',
|
||||||
'part_partparameter',
|
'part_partparameter',
|
||||||
'part_partrelated',
|
'part_partrelated',
|
||||||
|
'part_partcategoryparametertemplate',
|
||||||
],
|
],
|
||||||
'stock': [
|
'stock': [
|
||||||
'stock_stockitem',
|
'stock_stockitem',
|
||||||
@ -108,6 +109,9 @@ class RuleSet(models.Model):
|
|||||||
'report_reportasset',
|
'report_reportasset',
|
||||||
'report_testreport',
|
'report_testreport',
|
||||||
'part_partstar',
|
'part_partstar',
|
||||||
|
|
||||||
|
# Third-party tables
|
||||||
|
'error_report_error',
|
||||||
'exchange_rate',
|
'exchange_rate',
|
||||||
'exchange_exchangebackend',
|
'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-admin-shell==0.1.2 # Python shell for the admin interface
|
||||||
django-money==1.1 # Django app for currency management
|
django-money==1.1 # Django app for currency management
|
||||||
certifi # Certifi is (most likely) installed through one of the requirements above
|
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
|
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 random
|
||||||
import string
|
import string
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
def apps():
|
def apps():
|
||||||
"""
|
"""
|
||||||
@ -238,6 +239,96 @@ def postgresql(c):
|
|||||||
c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev')
|
c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev')
|
||||||
c.run('pip3 install psycopg2')
|
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
|
@task
|
||||||
def backup(c):
|
def backup(c):
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user