mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-03 13:58:47 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-app-refactor
This commit is contained in:
commit
47673c293e
19
.github/workflows/qc_checks.yaml
vendored
19
.github/workflows/qc_checks.yaml
vendored
@ -124,6 +124,16 @@ jobs:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
wrapper_name: inventree-python
|
wrapper_name: inventree-python
|
||||||
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
|
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
|
||||||
|
INVENTREE_MEDIA_ROOT: ../test_inventree_media
|
||||||
|
INVENTREE_STATIC_ROOT: ../test_inventree_static
|
||||||
|
INVENTREE_ADMIN_USER: testuser
|
||||||
|
INVENTREE_ADMIN_PASSWORD: testpassword
|
||||||
|
INVENTREE_ADMIN_EMAIL: test@test.com
|
||||||
|
INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345
|
||||||
|
INVENTREE_PYTHON_TEST_USERNAME: testuser
|
||||||
|
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
@ -140,13 +150,14 @@ jobs:
|
|||||||
git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
|
git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
|
||||||
- name: Start Server
|
- name: Start Server
|
||||||
run: |
|
run: |
|
||||||
invoke import-records -f ./${{ env.wrapper_name }}/test/test_data.json
|
invoke delete-data -f
|
||||||
invoke server -a 127.0.0.1:8000 &
|
invoke import-fixtures
|
||||||
sleep ${{ env.server_start_sleep }}
|
invoke server -a 127.0.0.1:12345 &
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
cd ${{ env.wrapper_name }}
|
cd ${{ env.wrapper_name }}
|
||||||
invoke test
|
invoke check-server
|
||||||
|
coverage run -m unittest discover -s test/
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
name: Sqlite / coverage
|
name: Sqlite / coverage
|
||||||
|
@ -57,6 +57,44 @@ class NotFoundView(AjaxView):
|
|||||||
return JsonResponse(data, status=404)
|
return JsonResponse(data, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
class APIDownloadMixin:
|
||||||
|
"""
|
||||||
|
Mixin for enabling a LIST endpoint to be downloaded a file.
|
||||||
|
|
||||||
|
To download the data, add the ?export=<fmt> to the query string.
|
||||||
|
|
||||||
|
The implementing class must provided a download_queryset method,
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
dataset = StockItemResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
|
||||||
|
date=datetime.now().strftime("%d-%b-%Y"),
|
||||||
|
fmt=export_format
|
||||||
|
)
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
export_format = request.query_params.get('export', None)
|
||||||
|
|
||||||
|
if export_format and export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
return self.download_queryset(queryset, export_format)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Default to the parent class implementation
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
raise NotImplementedError("download_queryset method not implemented!")
|
||||||
|
|
||||||
|
|
||||||
class AttachmentMixin:
|
class AttachmentMixin:
|
||||||
"""
|
"""
|
||||||
Mixin for creating attachment objects,
|
Mixin for creating attachment objects,
|
||||||
|
@ -4,11 +4,16 @@ InvenTree API version information
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 47
|
INVENTREE_API_VERSION = 48
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v48 -> 2022-05-12 : https://github.com/inventree/InvenTree/pull/2977
|
||||||
|
- Adds "export to file" functionality for PurchaseOrder API endpoint
|
||||||
|
- Adds "export to file" functionality for SalesOrder API endpoint
|
||||||
|
- Adds "export to file" functionality for BuildOrder API endpoint
|
||||||
|
|
||||||
v47 -> 2022-05-10 : https://github.com/inventree/InvenTree/pull/2964
|
v47 -> 2022-05-10 : https://github.com/inventree/InvenTree/pull/2964
|
||||||
- Fixes barcode API error response when scanning a StockItem which does not exist
|
- Fixes barcode API error response when scanning a StockItem which does not exist
|
||||||
- Fixes barcode API error response when scanning a StockLocation which does not exist
|
- Fixes barcode API error response when scanning a StockLocation which does not exist
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
from django.shortcuts import HttpResponseRedirect
|
# -*- coding: utf-8 -*-
|
||||||
from django.urls import reverse_lazy, Resolver404
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import include, re_path
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
|
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import HttpResponseRedirect
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse_lazy, Resolver404
|
||||||
|
from django.urls import include, re_path
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -82,11 +85,23 @@ class AuthRequiredMiddleware(object):
|
|||||||
reverse_lazy('admin:logout'),
|
reverse_lazy('admin:logout'),
|
||||||
]
|
]
|
||||||
|
|
||||||
if path not in urls and not path.startswith('/api/'):
|
# Do not redirect requests to any of these paths
|
||||||
|
paths_ignore = [
|
||||||
|
'/api/',
|
||||||
|
'/js/',
|
||||||
|
'/media/',
|
||||||
|
'/static/',
|
||||||
|
]
|
||||||
|
|
||||||
|
if path not in urls and not any([path.startswith(p) for p in paths_ignore]):
|
||||||
# Save the 'next' parameter to pass through to the login view
|
# Save the 'next' parameter to pass through to the login view
|
||||||
|
|
||||||
return redirect('{}?next={}'.format(reverse_lazy('account_login'), request.path))
|
return redirect('{}?next={}'.format(reverse_lazy('account_login'), request.path))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Return a 401 (Unauthorized) response code for this request
|
||||||
|
return HttpResponse('Unauthorized', status=401)
|
||||||
|
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -2,9 +2,53 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from import_export.admin import ImportExportModelAdmin
|
|
||||||
|
|
||||||
from .models import Build, BuildItem
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
from import_export.fields import Field
|
||||||
|
from import_export.resources import ModelResource
|
||||||
|
import import_export.widgets as widgets
|
||||||
|
|
||||||
|
from build.models import Build, BuildItem
|
||||||
|
|
||||||
|
import part.models
|
||||||
|
|
||||||
|
|
||||||
|
class BuildResource(ModelResource):
|
||||||
|
"""Class for managing import/export of Build data"""
|
||||||
|
# For some reason, we need to specify the fields individually for this ModelResource,
|
||||||
|
# but we don't for other ones.
|
||||||
|
# TODO: 2022-05-12 - Need to investigate why this is the case!
|
||||||
|
|
||||||
|
pk = Field(attribute='pk')
|
||||||
|
|
||||||
|
reference = Field(attribute='reference')
|
||||||
|
|
||||||
|
title = Field(attribute='title')
|
||||||
|
|
||||||
|
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(part.models.Part))
|
||||||
|
|
||||||
|
part_name = Field(attribute='part__full_name', readonly=True)
|
||||||
|
|
||||||
|
overdue = Field(attribute='is_overdue', readonly=True, widget=widgets.BooleanWidget())
|
||||||
|
|
||||||
|
completed = Field(attribute='completed', readonly=True)
|
||||||
|
|
||||||
|
quantity = Field(attribute='quantity')
|
||||||
|
|
||||||
|
status = Field(attribute='status')
|
||||||
|
|
||||||
|
batch = Field(attribute='batch')
|
||||||
|
|
||||||
|
notes = Field(attribute='notes')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
models = Build
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
clean_model_instances = True
|
||||||
|
exclude = [
|
||||||
|
'lft', 'rght', 'tree_id', 'level',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class BuildAdmin(ImportExportModelAdmin):
|
class BuildAdmin(ImportExportModelAdmin):
|
||||||
|
@ -12,13 +12,15 @@ from rest_framework import filters, generics
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
|
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin, APIDownloadMixin
|
||||||
from InvenTree.helpers import str2bool, isNull
|
from InvenTree.helpers import str2bool, isNull, DownloadFile
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
|
|
||||||
from .models import Build, BuildItem, BuildOrderAttachment
|
import build.admin
|
||||||
import build.serializers
|
import build.serializers
|
||||||
|
from build.models import Build, BuildItem, BuildOrderAttachment
|
||||||
|
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
|
|
||||||
@ -71,7 +73,7 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class BuildList(generics.ListCreateAPIView):
|
class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of Build objects.
|
""" API endpoint for accessing a list of Build objects.
|
||||||
|
|
||||||
- GET: Return list of objects (with filters)
|
- GET: Return list of objects (with filters)
|
||||||
@ -123,6 +125,14 @@ class BuildList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
dataset = build.admin.BuildResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
filename = f"InvenTree_BuildOrders.{export_format}"
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
@ -17,15 +17,16 @@ import base64
|
|||||||
from secrets import compare_digest
|
from secrets import compare_digest
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
from django.db.utils import IntegrityError, OperationalError
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.utils import IntegrityError, OperationalError
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||||
from django.conf import settings
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
|
||||||
|
|
||||||
from djmoney.settings import CURRENCY_CHOICES
|
from djmoney.settings import CURRENCY_CHOICES
|
||||||
from djmoney.contrib.exchange.models import convert_money
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
@ -136,6 +137,19 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
def get_kwargs(self):
|
||||||
|
"""
|
||||||
|
Construct kwargs for doing class-based settings lookup,
|
||||||
|
depending on *which* class we are.
|
||||||
|
|
||||||
|
This is necessary to abtract the settings object
|
||||||
|
from the implementing class (e.g plugins)
|
||||||
|
|
||||||
|
Subclasses should override this function to ensure the kwargs are correctly set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_definition(cls, key, **kwargs):
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -319,11 +333,11 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
value = setting.value
|
value = setting.value
|
||||||
|
|
||||||
# Cast to boolean if necessary
|
# Cast to boolean if necessary
|
||||||
if setting.is_bool(**kwargs):
|
if setting.is_bool():
|
||||||
value = InvenTree.helpers.str2bool(value)
|
value = InvenTree.helpers.str2bool(value)
|
||||||
|
|
||||||
# Cast to integer if necessary
|
# Cast to integer if necessary
|
||||||
if setting.is_int(**kwargs):
|
if setting.is_int():
|
||||||
try:
|
try:
|
||||||
value = int(value)
|
value = int(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
@ -390,19 +404,19 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.__class__.get_setting_name(self.key)
|
return self.__class__.get_setting_name(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_value(self):
|
def default_value(self):
|
||||||
return self.__class__.get_setting_default(self.key)
|
return self.__class__.get_setting_default(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
return self.__class__.get_setting_description(self.key)
|
return self.__class__.get_setting_description(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def units(self):
|
def units(self):
|
||||||
return self.__class__.get_setting_units(self.key)
|
return self.__class__.get_setting_units(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
def clean(self, **kwargs):
|
def clean(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -512,12 +526,12 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
except self.DoesNotExist:
|
except self.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def choices(self, **kwargs):
|
def choices(self):
|
||||||
"""
|
"""
|
||||||
Return the available choices for this setting (or None if no choices are defined)
|
Return the available choices for this setting (or None if no choices are defined)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.__class__.get_setting_choices(self.key, **kwargs)
|
return self.__class__.get_setting_choices(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
def valid_options(self):
|
def valid_options(self):
|
||||||
"""
|
"""
|
||||||
@ -531,14 +545,14 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return [opt[0] for opt in choices]
|
return [opt[0] for opt in choices]
|
||||||
|
|
||||||
def is_choice(self, **kwargs):
|
def is_choice(self):
|
||||||
"""
|
"""
|
||||||
Check if this setting is a "choice" field
|
Check if this setting is a "choice" field
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.__class__.get_setting_choices(self.key, **kwargs) is not None
|
return self.__class__.get_setting_choices(self.key, **self.get_kwargs()) is not None
|
||||||
|
|
||||||
def as_choice(self, **kwargs):
|
def as_choice(self):
|
||||||
"""
|
"""
|
||||||
Render this setting as the "display" value of a choice field,
|
Render this setting as the "display" value of a choice field,
|
||||||
e.g. if the choices are:
|
e.g. if the choices are:
|
||||||
@ -547,7 +561,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
then display 'A4 paper'
|
then display 'A4 paper'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
choices = self.get_setting_choices(self.key, **kwargs)
|
choices = self.get_setting_choices(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
if not choices:
|
if not choices:
|
||||||
return self.value
|
return self.value
|
||||||
@ -558,12 +572,80 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def is_bool(self, **kwargs):
|
def is_model(self):
|
||||||
|
"""
|
||||||
|
Check if this setting references a model instance in the database
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.model_name() is not None
|
||||||
|
|
||||||
|
def model_name(self):
|
||||||
|
"""
|
||||||
|
Return the model name associated with this setting
|
||||||
|
"""
|
||||||
|
|
||||||
|
setting = self.get_setting_definition(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
|
return setting.get('model', None)
|
||||||
|
|
||||||
|
def model_class(self):
|
||||||
|
"""
|
||||||
|
Return the model class associated with this setting, if (and only if):
|
||||||
|
|
||||||
|
- It has a defined 'model' parameter
|
||||||
|
- The 'model' parameter is of the form app.model
|
||||||
|
- The 'model' parameter has matches a known app model
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_name = self.model_name()
|
||||||
|
|
||||||
|
if not model_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
(app, mdl) = model_name.strip().split('.')
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"Invalid 'model' parameter for setting {self.key} : '{model_name}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
app_models = apps.all_models.get(app, None)
|
||||||
|
|
||||||
|
if app_models is None:
|
||||||
|
logger.error(f"Error retrieving model class '{model_name}' for setting '{self.key}' - no app named '{app}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
model = app_models.get(mdl, None)
|
||||||
|
|
||||||
|
if model is None:
|
||||||
|
logger.error(f"Error retrieving model class '{model_name}' for setting '{self.key}' - no model named '{mdl}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Looks like we have found a model!
|
||||||
|
return model
|
||||||
|
|
||||||
|
def api_url(self):
|
||||||
|
"""
|
||||||
|
Return the API url associated with the linked model,
|
||||||
|
if provided, and valid!
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_class = self.model_class()
|
||||||
|
|
||||||
|
if model_class:
|
||||||
|
# If a valid class has been found, see if it has registered an API URL
|
||||||
|
try:
|
||||||
|
return model_class.get_api_url()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_bool(self):
|
||||||
"""
|
"""
|
||||||
Check if this setting is required to be a boolean value
|
Check if this setting is required to be a boolean value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
validator = self.__class__.get_setting_validator(self.key, **kwargs)
|
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
return self.__class__.validator_is_bool(validator)
|
return self.__class__.validator_is_bool(validator)
|
||||||
|
|
||||||
@ -576,17 +658,20 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return InvenTree.helpers.str2bool(self.value)
|
return InvenTree.helpers.str2bool(self.value)
|
||||||
|
|
||||||
def setting_type(self, **kwargs):
|
def setting_type(self):
|
||||||
"""
|
"""
|
||||||
Return the field type identifier for this setting object
|
Return the field type identifier for this setting object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.is_bool(**kwargs):
|
if self.is_bool():
|
||||||
return 'boolean'
|
return 'boolean'
|
||||||
|
|
||||||
elif self.is_int(**kwargs):
|
elif self.is_int():
|
||||||
return 'integer'
|
return 'integer'
|
||||||
|
|
||||||
|
elif self.is_model():
|
||||||
|
return 'related field'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return 'string'
|
return 'string'
|
||||||
|
|
||||||
@ -603,12 +688,12 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_int(self, **kwargs):
|
def is_int(self,):
|
||||||
"""
|
"""
|
||||||
Check if the setting is required to be an integer value:
|
Check if the setting is required to be an integer value:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
validator = self.__class__.get_setting_validator(self.key, **kwargs)
|
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
return self.__class__.validator_is_int(validator)
|
return self.__class__.validator_is_int(validator)
|
||||||
|
|
||||||
@ -651,88 +736,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def protected(self):
|
def protected(self):
|
||||||
return self.__class__.is_protected(self.key)
|
return self.__class__.is_protected(self.key, **self.get_kwargs())
|
||||||
|
|
||||||
|
|
||||||
class GenericReferencedSettingClass:
|
|
||||||
"""
|
|
||||||
This mixin can be used to add reference keys to static properties
|
|
||||||
|
|
||||||
Sample:
|
|
||||||
```python
|
|
||||||
class SampleSetting(GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
|
||||||
class Meta:
|
|
||||||
unique_together = [
|
|
||||||
('sample', 'key'),
|
|
||||||
]
|
|
||||||
|
|
||||||
REFERENCE_NAME = 'sample'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_setting_definition(cls, key, **kwargs):
|
|
||||||
# mysampledict contains the dict with all settings for this SettingClass - this could also be a dynamic lookup
|
|
||||||
|
|
||||||
kwargs['settings'] = mysampledict
|
|
||||||
return super().get_setting_definition(key, **kwargs)
|
|
||||||
|
|
||||||
sample = models.charKey( # the name for this field is the additonal key and must be set in the Meta class an REFERENCE_NAME
|
|
||||||
max_length=256,
|
|
||||||
verbose_name=_('sample')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
REFERENCE_NAME = None
|
|
||||||
|
|
||||||
def _get_reference(self):
|
|
||||||
"""
|
|
||||||
Returns dict that can be used as an argument for kwargs calls.
|
|
||||||
Helps to make overriden calls generic for simple reuse.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```python
|
|
||||||
some_random_function(argument0, kwarg1=value1, **self._get_reference())
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
self.REFERENCE_NAME: getattr(self, self.REFERENCE_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
We override the following class methods,
|
|
||||||
so that we can pass the modified key instance as an additional argument
|
|
||||||
"""
|
|
||||||
|
|
||||||
def clean(self, **kwargs):
|
|
||||||
|
|
||||||
kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME)
|
|
||||||
|
|
||||||
super().clean(**kwargs)
|
|
||||||
|
|
||||||
def is_bool(self, **kwargs):
|
|
||||||
|
|
||||||
kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME)
|
|
||||||
|
|
||||||
return super().is_bool(**kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.__class__.get_setting_name(self.key, **self._get_reference())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def default_value(self):
|
|
||||||
return self.__class__.get_setting_default(self.key, **self._get_reference())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
return self.__class__.get_setting_description(self.key, **self._get_reference())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def units(self):
|
|
||||||
return self.__class__.get_setting_units(self.key, **self._get_reference())
|
|
||||||
|
|
||||||
def choices(self):
|
|
||||||
return self.__class__.get_setting_choices(self.key, **self._get_reference())
|
|
||||||
|
|
||||||
|
|
||||||
def settings_group_options():
|
def settings_group_options():
|
||||||
@ -1558,6 +1562,16 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
|
|
||||||
return self.__class__.get_setting(self.key, user=self.user)
|
return self.__class__.get_setting(self.key, user=self.user)
|
||||||
|
|
||||||
|
def get_kwargs(self):
|
||||||
|
"""
|
||||||
|
Explicit kwargs required to uniquely identify a particular setting object,
|
||||||
|
in addition to the 'key' parameter
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
'user': self.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PriceBreak(models.Model):
|
class PriceBreak(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -28,6 +28,10 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
choices = serializers.SerializerMethodField()
|
choices = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
model_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
api_url = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def get_choices(self, obj):
|
def get_choices(self, obj):
|
||||||
"""
|
"""
|
||||||
Returns the choices available for a given item
|
Returns the choices available for a given item
|
||||||
@ -75,6 +79,8 @@ class GlobalSettingsSerializer(SettingsSerializer):
|
|||||||
'description',
|
'description',
|
||||||
'type',
|
'type',
|
||||||
'choices',
|
'choices',
|
||||||
|
'model_name',
|
||||||
|
'api_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -96,6 +102,8 @@ class UserSettingsSerializer(SettingsSerializer):
|
|||||||
'user',
|
'user',
|
||||||
'type',
|
'type',
|
||||||
'choices',
|
'choices',
|
||||||
|
'model_name',
|
||||||
|
'api_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -124,6 +132,8 @@ class GenericReferencedSettingSerializer(SettingsSerializer):
|
|||||||
'description',
|
'description',
|
||||||
'type',
|
'type',
|
||||||
'choices',
|
'choices',
|
||||||
|
'model_name',
|
||||||
|
'api_url',
|
||||||
]
|
]
|
||||||
|
|
||||||
# set Meta class
|
# set Meta class
|
||||||
|
@ -5,8 +5,9 @@ from django.contrib import admin
|
|||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from import_export.resources import ModelResource
|
|
||||||
from import_export.fields import Field
|
from import_export.fields import Field
|
||||||
|
from import_export.resources import ModelResource
|
||||||
|
import import_export.widgets as widgets
|
||||||
|
|
||||||
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine
|
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine
|
||||||
from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine
|
from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine
|
||||||
@ -92,6 +93,23 @@ class SalesOrderAdmin(ImportExportModelAdmin):
|
|||||||
autocomplete_fields = ('customer',)
|
autocomplete_fields = ('customer',)
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderResource(ModelResource):
|
||||||
|
"""
|
||||||
|
Class for managing import / export of PurchaseOrder data
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add number of line items
|
||||||
|
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
|
||||||
|
|
||||||
|
# Is this order overdue?
|
||||||
|
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PurchaseOrder
|
||||||
|
skip_unchanged = True
|
||||||
|
clean_model_instances = True
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItemResource(ModelResource):
|
class PurchaseOrderLineItemResource(ModelResource):
|
||||||
""" Class for managing import / export of PurchaseOrderLineItem data """
|
""" Class for managing import / export of PurchaseOrderLineItem data """
|
||||||
|
|
||||||
@ -117,6 +135,23 @@ class PurchaseOrderExtraLineResource(ModelResource):
|
|||||||
model = PurchaseOrderExtraLine
|
model = PurchaseOrderExtraLine
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderResource(ModelResource):
|
||||||
|
"""
|
||||||
|
Class for managing import / export of SalesOrder data
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add number of line items
|
||||||
|
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
|
||||||
|
|
||||||
|
# Is this order overdue?
|
||||||
|
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SalesOrder
|
||||||
|
skip_unchanged = True
|
||||||
|
clean_model_instances = True
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderLineItemResource(ModelResource):
|
class SalesOrderLineItemResource(ModelResource):
|
||||||
"""
|
"""
|
||||||
Class for managing import / export of SalesOrderLineItem data
|
Class for managing import / export of SalesOrderLineItem data
|
||||||
|
@ -17,10 +17,11 @@ from company.models import SupplierPart
|
|||||||
|
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.helpers import str2bool, DownloadFile
|
from InvenTree.helpers import str2bool, DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin, APIDownloadMixin
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
||||||
|
|
||||||
from order.admin import PurchaseOrderLineItemResource
|
from order.admin import PurchaseOrderResource, PurchaseOrderLineItemResource
|
||||||
|
from order.admin import SalesOrderResource
|
||||||
import order.models as models
|
import order.models as models
|
||||||
import order.serializers as serializers
|
import order.serializers as serializers
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@ -110,7 +111,7 @@ class PurchaseOrderFilter(rest_filters.FilterSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderList(generics.ListCreateAPIView):
|
class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PurchaseOrder objects
|
""" API endpoint for accessing a list of PurchaseOrder objects
|
||||||
|
|
||||||
- GET: Return list of PurchaseOrder objects (with filters)
|
- GET: Return list of PurchaseOrder objects (with filters)
|
||||||
@ -160,6 +161,15 @@ class PurchaseOrderList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
dataset = PurchaseOrderResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = f"InvenTree_PurchaseOrders.{export_format}"
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
|
||||||
# Perform basic filtering
|
# Perform basic filtering
|
||||||
@ -407,7 +417,7 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItemList(generics.ListCreateAPIView):
|
class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PurchaseOrderLineItem objects
|
""" API endpoint for accessing a list of PurchaseOrderLineItem objects
|
||||||
|
|
||||||
- GET: Return a list of PurchaseOrder Line Item objects
|
- GET: Return a list of PurchaseOrder Line Item objects
|
||||||
@ -460,25 +470,19 @@ class PurchaseOrderLineItemList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = f"InvenTree_PurchaseOrderItems.{export_format}"
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
# Check if we wish to export the queried data to a file
|
|
||||||
export_format = request.query_params.get('export', None)
|
|
||||||
|
|
||||||
if export_format:
|
|
||||||
export_format = str(export_format).strip().lower()
|
|
||||||
|
|
||||||
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
|
||||||
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
|
||||||
|
|
||||||
filename = f"InvenTree_PurchaseOrderData.{export_format}"
|
|
||||||
|
|
||||||
return DownloadFile(filedata, filename)
|
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
if page is not None:
|
if page is not None:
|
||||||
@ -580,7 +584,7 @@ class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, Attachme
|
|||||||
serializer_class = serializers.SalesOrderAttachmentSerializer
|
serializer_class = serializers.SalesOrderAttachmentSerializer
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderList(generics.ListCreateAPIView):
|
class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for accessing a list of SalesOrder objects.
|
API endpoint for accessing a list of SalesOrder objects.
|
||||||
|
|
||||||
@ -630,6 +634,15 @@ class SalesOrderList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
dataset = SalesOrderResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = f"InvenTree_SalesOrders.{export_format}"
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
"""
|
"""
|
||||||
Perform custom filtering operations on the SalesOrder queryset.
|
Perform custom filtering operations on the SalesOrder queryset.
|
||||||
|
@ -4,8 +4,8 @@ from __future__ import unicode_literals
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
from import_export.resources import ModelResource
|
|
||||||
from import_export.fields import Field
|
from import_export.fields import Field
|
||||||
|
from import_export.resources import ModelResource
|
||||||
import import_export.widgets as widgets
|
import import_export.widgets as widgets
|
||||||
|
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
|
@ -49,7 +49,7 @@ from . import serializers as part_serializers
|
|||||||
|
|
||||||
from InvenTree.helpers import str2bool, isNull, increment
|
from InvenTree.helpers import str2bool, isNull, increment
|
||||||
from InvenTree.helpers import DownloadFile
|
from InvenTree.helpers import DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin, APIDownloadMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||||
|
|
||||||
@ -847,7 +847,7 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
virtual = rest_filters.BooleanFilter()
|
virtual = rest_filters.BooleanFilter()
|
||||||
|
|
||||||
|
|
||||||
class PartList(generics.ListCreateAPIView):
|
class PartList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||||
"""
|
"""
|
||||||
API endpoint for accessing a list of Part objects
|
API endpoint for accessing a list of Part objects
|
||||||
|
|
||||||
@ -897,6 +897,14 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
dataset = PartResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
filename = f"InvenTree_Parts.{export_format}"
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Overide the 'list' method, as the PartCategory objects are
|
Overide the 'list' method, as the PartCategory objects are
|
||||||
@ -908,22 +916,6 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
# Check if we wish to export the queried data to a file.
|
|
||||||
# If so, skip pagination!
|
|
||||||
export_format = request.query_params.get('export', None)
|
|
||||||
|
|
||||||
if export_format:
|
|
||||||
export_format = str(export_format).strip().lower()
|
|
||||||
|
|
||||||
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
|
||||||
dataset = PartResource().export(queryset=queryset)
|
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
|
||||||
|
|
||||||
filename = f"InvenTree_Parts.{export_format}"
|
|
||||||
|
|
||||||
return DownloadFile(filedata, filename)
|
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
@ -2233,7 +2233,7 @@ class Part(MPTTModel):
|
|||||||
for child in children:
|
for child in children:
|
||||||
parts.append(child)
|
parts.append(child)
|
||||||
|
|
||||||
# Immediate parent
|
# Immediate parent, and siblings
|
||||||
if self.variant_of:
|
if self.variant_of:
|
||||||
parts.append(self.variant_of)
|
parts.append(self.variant_of)
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ class PluginConfig(models.Model):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class PluginSetting(common.models.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||||
"""
|
"""
|
||||||
This model represents settings for individual plugins
|
This model represents settings for individual plugins
|
||||||
"""
|
"""
|
||||||
@ -116,7 +116,13 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B
|
|||||||
('plugin', 'key'),
|
('plugin', 'key'),
|
||||||
]
|
]
|
||||||
|
|
||||||
REFERENCE_NAME = 'plugin'
|
plugin = models.ForeignKey(
|
||||||
|
PluginConfig,
|
||||||
|
related_name='settings',
|
||||||
|
null=False,
|
||||||
|
verbose_name=_('Plugin'),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_definition(cls, key, **kwargs):
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
@ -146,16 +152,18 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B
|
|||||||
|
|
||||||
return super().get_setting_definition(key, **kwargs)
|
return super().get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
plugin = models.ForeignKey(
|
def get_kwargs(self):
|
||||||
PluginConfig,
|
"""
|
||||||
related_name='settings',
|
Explicit kwargs required to uniquely identify a particular setting object,
|
||||||
null=False,
|
in addition to the 'key' parameter
|
||||||
verbose_name=_('Plugin'),
|
"""
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
return {
|
||||||
|
'plugin': self.plugin,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotificationUserSetting(common.models.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
class NotificationUserSetting(common.models.BaseInvenTreeSetting):
|
||||||
"""
|
"""
|
||||||
This model represents notification settings for a user
|
This model represents notification settings for a user
|
||||||
"""
|
"""
|
||||||
@ -165,8 +173,6 @@ class NotificationUserSetting(common.models.GenericReferencedSettingClass, commo
|
|||||||
('method', 'user', 'key'),
|
('method', 'user', 'key'),
|
||||||
]
|
]
|
||||||
|
|
||||||
REFERENCE_NAME = 'method'
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_definition(cls, key, **kwargs):
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
from common.notifications import storage
|
from common.notifications import storage
|
||||||
@ -175,6 +181,17 @@ class NotificationUserSetting(common.models.GenericReferencedSettingClass, commo
|
|||||||
|
|
||||||
return super().get_setting_definition(key, **kwargs)
|
return super().get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
|
def get_kwargs(self):
|
||||||
|
"""
|
||||||
|
Explicit kwargs required to uniquely identify a particular setting object,
|
||||||
|
in addition to the 'key' parameter
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
'method': self.method,
|
||||||
|
'user': self.user,
|
||||||
|
}
|
||||||
|
|
||||||
method = models.CharField(
|
method = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_('Method'),
|
verbose_name=_('Method'),
|
||||||
|
@ -65,6 +65,16 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
|
|||||||
],
|
],
|
||||||
'default': 'A',
|
'default': 'A',
|
||||||
},
|
},
|
||||||
|
'SELECT_COMPANY': {
|
||||||
|
'name': 'Company',
|
||||||
|
'description': 'Select a company object from the database',
|
||||||
|
'model': 'company.company',
|
||||||
|
},
|
||||||
|
'SELECT_PART': {
|
||||||
|
'name': 'Part',
|
||||||
|
'description': 'Select a part object from the database',
|
||||||
|
'model': 'part.part',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
NAVIGATION = [
|
NAVIGATION = [
|
||||||
|
@ -33,7 +33,7 @@ from company.serializers import CompanySerializer, SupplierPartSerializer
|
|||||||
|
|
||||||
from InvenTree.helpers import str2bool, isNull, extract_serial_numbers
|
from InvenTree.helpers import str2bool, isNull, extract_serial_numbers
|
||||||
from InvenTree.helpers import DownloadFile
|
from InvenTree.helpers import DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin, APIDownloadMixin
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
|
|
||||||
from order.models import PurchaseOrder
|
from order.models import PurchaseOrder
|
||||||
@ -505,7 +505,7 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
updated_after = rest_filters.DateFilter(label='Updated after', field_name='updated', lookup_expr='gte')
|
updated_after = rest_filters.DateFilter(label='Updated after', field_name='updated', lookup_expr='gte')
|
||||||
|
|
||||||
|
|
||||||
class StockList(generics.ListCreateAPIView):
|
class StockList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||||
""" API endpoint for list view of Stock objects
|
""" API endpoint for list view of Stock objects
|
||||||
|
|
||||||
- GET: Return a list of all StockItem objects (with optional query filters)
|
- GET: Return a list of all StockItem objects (with optional query filters)
|
||||||
@ -646,6 +646,22 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data))
|
return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data))
|
||||||
|
|
||||||
|
def download_queryset(self, queryset, export_format):
|
||||||
|
"""
|
||||||
|
Download this queryset as a file.
|
||||||
|
Uses the APIDownloadMixin mixin class
|
||||||
|
"""
|
||||||
|
dataset = StockItemResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = 'InvenTree_StockItems_{date}.{fmt}'.format(
|
||||||
|
date=datetime.now().strftime("%d-%b-%Y"),
|
||||||
|
fmt=export_format
|
||||||
|
)
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Override the 'list' method, as the StockLocation objects
|
Override the 'list' method, as the StockLocation objects
|
||||||
@ -658,25 +674,6 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
params = request.query_params
|
params = request.query_params
|
||||||
|
|
||||||
# Check if we wish to export the queried data to a file.
|
|
||||||
# If so, skip pagination!
|
|
||||||
export_format = params.get('export', None)
|
|
||||||
|
|
||||||
if export_format:
|
|
||||||
export_format = str(export_format).strip().lower()
|
|
||||||
|
|
||||||
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
|
||||||
dataset = StockItemResource().export(queryset=queryset)
|
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
|
||||||
|
|
||||||
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
|
|
||||||
date=datetime.now().strftime("%d-%b-%Y"),
|
|
||||||
fmt=export_format
|
|
||||||
)
|
|
||||||
|
|
||||||
return DownloadFile(filedata, filename)
|
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
@ -556,7 +556,14 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
# If the item points to a build, check that the Part references match
|
# If the item points to a build, check that the Part references match
|
||||||
if self.build:
|
if self.build:
|
||||||
if not self.part == self.build.part:
|
|
||||||
|
if self.part == self.build.part:
|
||||||
|
# Part references match exactly
|
||||||
|
pass
|
||||||
|
elif self.part in self.build.part.get_conversion_options():
|
||||||
|
# Part reference is one of the valid conversion options for the build output
|
||||||
|
pass
|
||||||
|
else:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'build': _("Build reference does not point to the same part object")
|
'build': _("Build reference does not point to the same part object")
|
||||||
})
|
})
|
||||||
|
@ -71,9 +71,24 @@ function editSetting(key, options={}) {
|
|||||||
help_text: response.description,
|
help_text: response.description,
|
||||||
type: response.type,
|
type: response.type,
|
||||||
choices: response.choices,
|
choices: response.choices,
|
||||||
|
value: response.value,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Foreign key lookup available!
|
||||||
|
if (response.type == 'related field') {
|
||||||
|
|
||||||
|
if (response.model_name && response.api_url) {
|
||||||
|
fields.value.type = 'related field';
|
||||||
|
fields.value.model = response.model_name.split('.').at(-1);
|
||||||
|
fields.value.api_url = response.api_url;
|
||||||
|
} else {
|
||||||
|
// Unknown / unsupported model type, default to 'text' field
|
||||||
|
fields.value.type = 'text';
|
||||||
|
console.warn(`Unsupported model type: '${response.model_name}' for setting '${response.key}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructChangeForm(fields, {
|
constructChangeForm(fields, {
|
||||||
url: url,
|
url: url,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
@ -2355,7 +2355,7 @@ function loadBuildTable(table, options) {
|
|||||||
|
|
||||||
var filterTarget = options.filterTarget || null;
|
var filterTarget = options.filterTarget || null;
|
||||||
|
|
||||||
setupFilterList('build', table, filterTarget);
|
setupFilterList('build', table, filterTarget, {download: true});
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
@ -1394,7 +1394,9 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
filters[key] = options.params[key];
|
filters[key] = options.params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilterList('purchaseorder', $(table));
|
var target = '#filter-list-purchaseorder';
|
||||||
|
|
||||||
|
setupFilterList('purchaseorder', $(table), target, {download: true});
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: '{% url "api-po-list" %}',
|
url: '{% url "api-po-list" %}',
|
||||||
@ -2091,7 +2093,9 @@ function loadSalesOrderTable(table, options) {
|
|||||||
|
|
||||||
options.url = options.url || '{% url "api-so-list" %}';
|
options.url = options.url || '{% url "api-so-list" %}';
|
||||||
|
|
||||||
setupFilterList('salesorder', $(table));
|
var target = '#filter-list-salesorder';
|
||||||
|
|
||||||
|
setupFilterList('salesorder', $(table), target, {download: true});
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user