mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-02 11:40:58 +00:00
Project code support (#4636)
* Support image uploads in the "notes" markdown fields - Implemented using the existing EasyMDE library - Copy / paste support - Drag / drop support * Remove debug message * Updated API version * Better UX when saving notes * Pin PIP version (for testing) * Bug fixes - Fix typo - Use correct serializer type * Add unit testing * Update role permissions * Typo fix * Update migration file * Adds a notes mixin class to be used for refactoring * Refactor existing models with notes to use the new mixin * Add helper function for finding all model types with a certain mixin * Refactor barcode plugin to use new method * Typo fix * Add daily task to delete old / unused notes * Add ProjectCode model (cherry picked from commit 382a0a2fc32c930d46ed3fe0c6d2cae654c2209d) * Adds IsStaffOrReadyOnly permissions - Authenticated users get read-only access - Staff users get read/write access (cherry picked from commit 53d04da86c4c866fd9c909d147d93844186470b4) * Adds API endpoints for project codes (cherry picked from commit 5ae1da23b2eae4e1168bc6fe28a3544dedc4a1b4) * Add migration file for projectcode model (cherry picked from commit 5f8717712c65df853ea69907d33e185fd91df7ee) * Add project code configuration page to the global settings view * Add 'project code' field to orders * Add ability to set / edit the project code for various order models * Add project code info to order list tables * Add configuration options for project code integration * Allow orders to be filtered by project code * Refactor table_filters.js - Allow orders to be filtered dynamically by project code * Bump API version * Fixes * Add resource mixin for exporting project code in order list * Add "has_project_code" filter * javascript fix * Edit / delete project codes via API - Also refactor some existing JS * Move MetadataMixin to InvenTree.models To prevent circular imports (cherry picked from commit d23b013881eaffe612dfbfcdfc5dff6d729068c6) * Fixes for circular imports * Add metadata for ProjectCode model * Add Metadata API endpoint for ProjectCode * Add unit testing for ProjectCode API endpoints
This commit is contained in:
@ -17,13 +17,13 @@ from rest_framework.views import APIView
|
||||
|
||||
import common.models
|
||||
import common.serializers
|
||||
from InvenTree.api import BulkDeleteMixin
|
||||
from InvenTree.api import BulkDeleteMixin, MetadataView
|
||||
from InvenTree.config import CONFIG_LOOKUPS
|
||||
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
|
||||
from InvenTree.helpers import inheritors
|
||||
from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI,
|
||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||
from InvenTree.permissions import IsSuperuser
|
||||
from InvenTree.permissions import IsStaffOrReadOnly, IsSuperuser
|
||||
from plugin.models import NotificationUserSetting
|
||||
from plugin.serializers import NotificationUserSettingSerializer
|
||||
|
||||
@ -454,6 +454,22 @@ class NotesImageList(ListCreateAPI):
|
||||
image.save()
|
||||
|
||||
|
||||
class ProjectCodeList(ListCreateAPI):
|
||||
"""List view for all project codes."""
|
||||
|
||||
queryset = common.models.ProjectCode.objects.all()
|
||||
serializer_class = common.serializers.ProjectCodeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
|
||||
|
||||
class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail view for a particular project code"""
|
||||
|
||||
queryset = common.models.ProjectCode.objects.all()
|
||||
serializer_class = common.serializers.ProjectCodeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
|
||||
|
||||
settings_api_urls = [
|
||||
# User settings
|
||||
re_path(r'^user/', include([
|
||||
@ -490,6 +506,15 @@ common_api_urls = [
|
||||
# Uploaded images for notes
|
||||
re_path(r'^notes-image-upload/', NotesImageList.as_view(), name='api-notes-image-list'),
|
||||
|
||||
# Project codes
|
||||
re_path(r'^project-code/', include([
|
||||
path(r'<int:pk>/', include([
|
||||
re_path(r'^metadata/', MetadataView.as_view(), {'model': common.models.ProjectCode}, name='api-project-code-metadata'),
|
||||
re_path(r'^.*$', ProjectCodeDetail.as_view(), name='api-project-code-detail'),
|
||||
])),
|
||||
re_path(r'^.*$', ProjectCodeList.as_view(), name='api-project-code-list'),
|
||||
])),
|
||||
|
||||
# Currencies
|
||||
re_path(r'^currency/', include([
|
||||
re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'),
|
||||
|
21
InvenTree/common/migrations/0018_projectcode.py
Normal file
21
InvenTree/common/migrations/0018_projectcode.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.2.18 on 2023-04-19 02:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0017_notesimage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProjectCode',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(help_text='Unique project code', max_length=50, unique=True, verbose_name='Project Code')),
|
||||
('description', models.CharField(blank=True, help_text='Project description', max_length=200, verbose_name='Description')),
|
||||
],
|
||||
),
|
||||
]
|
18
InvenTree/common/migrations/0019_projectcode_metadata.py
Normal file
18
InvenTree/common/migrations/0019_projectcode_metadata.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.18 on 2023-04-19 13:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0018_projectcode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='projectcode',
|
||||
name='metadata',
|
||||
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||
),
|
||||
]
|
@ -42,6 +42,7 @@ from rest_framework.exceptions import PermissionDenied
|
||||
import build.validators
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
import InvenTree.validators
|
||||
@ -84,6 +85,33 @@ class EmptyURLValidator(URLValidator):
|
||||
super().__call__(value)
|
||||
|
||||
|
||||
class ProjectCode(InvenTree.models.MetadataMixin, models.Model):
|
||||
"""A ProjectCode is a unique identifier for a project."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL for this model."""
|
||||
return reverse('api-project-code-list')
|
||||
|
||||
def __str__(self):
|
||||
"""String representation of a ProjectCode."""
|
||||
return self.code
|
||||
|
||||
code = models.CharField(
|
||||
max_length=50,
|
||||
unique=True,
|
||||
verbose_name=_('Project Code'),
|
||||
help_text=_('Unique project code'),
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
verbose_name=_('Description'),
|
||||
help_text=_('Project description'),
|
||||
)
|
||||
|
||||
|
||||
class BaseInvenTreeSetting(models.Model):
|
||||
"""An base InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values)."""
|
||||
|
||||
@ -1631,6 +1659,13 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
"PROJECT_CODES_ENABLED": {
|
||||
'name': _('Enable project codes'),
|
||||
'description': _('Enable project codes for tracking projects'),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'STOCKTAKE_ENABLE': {
|
||||
'name': _('Stocktake Functionality'),
|
||||
'description': _('Enable stocktake functionality for recording stock levels and calculating stock value'),
|
||||
|
@ -5,7 +5,8 @@ from django.urls import reverse
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
||||
NewsFeedEntry, NotesImage, NotificationMessage)
|
||||
NewsFeedEntry, NotesImage, NotificationMessage,
|
||||
ProjectCode)
|
||||
from InvenTree.helpers import construct_absolute_url, get_objectreference
|
||||
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
||||
InvenTreeModelSerializer)
|
||||
@ -253,3 +254,17 @@ class NotesImageSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
image = InvenTreeImageSerializerField(required=True)
|
||||
|
||||
|
||||
class ProjectCodeSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for the ProjectCode model."""
|
||||
|
||||
class Meta:
|
||||
"""Meta options for ProjectCodeSerializer."""
|
||||
|
||||
model = ProjectCode
|
||||
fields = [
|
||||
'pk',
|
||||
'code',
|
||||
'description'
|
||||
]
|
||||
|
@ -22,7 +22,7 @@ from plugin.models import NotificationUserSetting
|
||||
from .api import WebhookView
|
||||
from .models import (ColorTheme, InvenTreeSetting, InvenTreeUserSetting,
|
||||
NotesImage, NotificationEntry, NotificationMessage,
|
||||
WebhookEndpoint, WebhookMessage)
|
||||
ProjectCode, WebhookEndpoint, WebhookMessage)
|
||||
|
||||
CONTENT_TYPE_JSON = 'application/json'
|
||||
|
||||
@ -1001,3 +1001,113 @@ class NotesImageTest(InvenTreeAPITestCase):
|
||||
|
||||
# Check that a new file has been created
|
||||
self.assertEqual(NotesImage.objects.count(), n + 1)
|
||||
|
||||
|
||||
class ProjectCodesTest(InvenTreeAPITestCase):
|
||||
"""Units tests for the ProjectCodes model and API endpoints"""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Return the URL for the project code list endpoint"""
|
||||
return reverse('api-project-code-list')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create some initial project codes"""
|
||||
super().setUpTestData()
|
||||
|
||||
codes = [
|
||||
ProjectCode(code='PRJ-001', description='Test project code'),
|
||||
ProjectCode(code='PRJ-002', description='Test project code'),
|
||||
ProjectCode(code='PRJ-003', description='Test project code'),
|
||||
ProjectCode(code='PRJ-004', description='Test project code'),
|
||||
]
|
||||
|
||||
ProjectCode.objects.bulk_create(codes)
|
||||
|
||||
def test_list(self):
|
||||
"""Test that the list endpoint works as expected"""
|
||||
|
||||
response = self.get(self.url, expected_code=200)
|
||||
self.assertEqual(len(response.data), ProjectCode.objects.count())
|
||||
|
||||
def test_delete(self):
|
||||
"""Test we can delete a project code via the API"""
|
||||
|
||||
n = ProjectCode.objects.count()
|
||||
|
||||
# Get the first project code
|
||||
code = ProjectCode.objects.first()
|
||||
|
||||
# Delete it
|
||||
self.delete(
|
||||
reverse('api-project-code-detail', kwargs={'pk': code.pk}),
|
||||
expected_code=204
|
||||
)
|
||||
|
||||
# Check it is gone
|
||||
self.assertEqual(ProjectCode.objects.count(), n - 1)
|
||||
|
||||
def test_duplicate_code(self):
|
||||
"""Test that we cannot create two project codes with the same code"""
|
||||
|
||||
# Create a new project code
|
||||
response = self.post(
|
||||
self.url,
|
||||
data={
|
||||
'code': 'PRJ-001',
|
||||
'description': 'Test project code',
|
||||
},
|
||||
expected_code=400
|
||||
)
|
||||
|
||||
self.assertIn('project code with this Project Code already exists', str(response.data['code']))
|
||||
|
||||
def test_write_access(self):
|
||||
"""Test that non-staff users have read-only access"""
|
||||
|
||||
# By default user has staff access, can create a new project code
|
||||
response = self.post(
|
||||
self.url,
|
||||
data={
|
||||
'code': 'PRJ-xxx',
|
||||
'description': 'Test project code',
|
||||
},
|
||||
expected_code=201
|
||||
)
|
||||
|
||||
pk = response.data['pk']
|
||||
|
||||
# Test we can edit, also
|
||||
response = self.patch(
|
||||
reverse('api-project-code-detail', kwargs={'pk': pk}),
|
||||
data={
|
||||
'code': 'PRJ-999',
|
||||
},
|
||||
expected_code=200
|
||||
)
|
||||
|
||||
self.assertEqual(response.data['code'], 'PRJ-999')
|
||||
|
||||
# Restrict user access to non-staff
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
|
||||
# As user does not have staff access, should return 403 for list endpoint
|
||||
response = self.post(
|
||||
self.url,
|
||||
data={
|
||||
'code': 'PRJ-123',
|
||||
'description': 'Test project code'
|
||||
},
|
||||
expected_code=403
|
||||
)
|
||||
|
||||
# Should also return 403 for detail endpoint
|
||||
response = self.patch(
|
||||
reverse('api-project-code-detail', kwargs={'pk': pk}),
|
||||
data={
|
||||
'code': 'PRJ-999',
|
||||
},
|
||||
expected_code=403
|
||||
)
|
||||
|
Reference in New Issue
Block a user