2
0
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:
Oliver
2023-04-20 00:47:07 +10:00
committed by GitHub
parent eafd2ac966
commit 070e2afcea
39 changed files with 1315 additions and 683 deletions

View File

@ -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'),

View 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')),
],
),
]

View 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'),
),
]

View File

@ -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'),

View File

@ -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'
]

View File

@ -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
)