mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
eafd2ac966
commit
070e2afcea
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 108
|
||||
INVENTREE_API_VERSION = 109
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v109 -> 2023-04-19 : https://github.com/inventree/InvenTree/pull/4636
|
||||
- Adds API endpoints for the "ProjectCode" model
|
||||
|
||||
v108 -> 2023-04-17 : https://github.com/inventree/InvenTree/pull/4615
|
||||
- Adds functionality to upload images for rendering in markdown notes
|
||||
|
||||
|
@ -12,8 +12,6 @@ from djmoney.models.fields import MoneyField as ModelMoneyField
|
||||
from djmoney.models.validators import MinMoneyValidator
|
||||
from rest_framework.fields import URLField as RestURLField
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
from .validators import AllowedURLValidator, allowable_url_schemes
|
||||
|
||||
|
||||
@ -150,6 +148,8 @@ class DatePickerFormField(forms.DateField):
|
||||
def round_decimal(value, places, normalize=False):
|
||||
"""Round value to the specified number of places."""
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
if type(value) in [Decimal, float]:
|
||||
value = round(value, places)
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
from django_filters import rest_framework as rest_filters
|
||||
from rest_framework import filters
|
||||
|
||||
from InvenTree.helpers import str2bool
|
||||
import InvenTree.helpers
|
||||
|
||||
|
||||
class InvenTreeSearchFilter(filters.SearchFilter):
|
||||
@ -16,7 +16,7 @@ class InvenTreeSearchFilter(filters.SearchFilter):
|
||||
- search_regex: If True, search is perfomed on 'regex' comparison
|
||||
"""
|
||||
|
||||
regex = str2bool(request.query_params.get('search_regex', False))
|
||||
regex = InvenTree.helpers.str2bool(request.query_params.get('search_regex', False))
|
||||
|
||||
search_fields = super().get_search_fields(view, request)
|
||||
|
||||
@ -37,7 +37,7 @@ class InvenTreeSearchFilter(filters.SearchFilter):
|
||||
Depending on the request parameters, we may "augment" these somewhat
|
||||
"""
|
||||
|
||||
whole = str2bool(request.query_params.get('search_whole', False))
|
||||
whole = InvenTree.helpers.str2bool(request.query_params.get('search_whole', False))
|
||||
|
||||
terms = []
|
||||
|
||||
|
@ -29,8 +29,8 @@ from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
import common.models
|
||||
import InvenTree.version
|
||||
from common.models import InvenTreeSetting
|
||||
from common.notifications import (InvenTreeNotificationBodies,
|
||||
NotificationBody, trigger_notification)
|
||||
from common.settings import currency_code_default
|
||||
@ -43,7 +43,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
def getSetting(key, backup_value=None):
|
||||
"""Shortcut for reading a setting value from the database."""
|
||||
return InvenTreeSetting.get_setting(key, backup_value=backup_value)
|
||||
return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value)
|
||||
|
||||
|
||||
def generateTestKey(test_name):
|
||||
@ -96,7 +96,7 @@ def construct_absolute_url(*arg):
|
||||
|
||||
This requires the BASE_URL configuration option to be set!
|
||||
"""
|
||||
base = str(InvenTreeSetting.get_setting('INVENTREE_BASE_URL'))
|
||||
base = str(common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL'))
|
||||
|
||||
url = '/'.join(arg)
|
||||
|
||||
@ -145,10 +145,10 @@ def download_image_from_url(remote_url, timeout=2.5):
|
||||
validator(remote_url)
|
||||
|
||||
# Calculate maximum allowable image size (in bytes)
|
||||
max_size = int(InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
|
||||
max_size = int(common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
|
||||
|
||||
# Add user specified user-agent to request (if specified)
|
||||
user_agent = InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
|
||||
user_agent = common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
|
||||
if user_agent:
|
||||
headers = {"User-Agent": user_agent}
|
||||
else:
|
||||
@ -1138,10 +1138,10 @@ def render_currency(money, decimal_places=None, currency=None, include_symbol=Tr
|
||||
pass
|
||||
|
||||
if decimal_places is None:
|
||||
decimal_places = InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
|
||||
if min_decimal_places is None:
|
||||
min_decimal_places = InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||
min_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||
|
||||
value = Decimal(str(money.amount)).normalize()
|
||||
value = str(value)
|
||||
|
@ -21,10 +21,10 @@ from error_report.models import Error
|
||||
from mptt.exceptions import InvalidMove
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
import common.models
|
||||
import InvenTree.fields
|
||||
import InvenTree.format
|
||||
import InvenTree.helpers
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.fields import InvenTreeNotesField, InvenTreeURLField
|
||||
from InvenTree.sanitizer import sanitize_svg
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
@ -44,6 +44,60 @@ def rename_attachment(instance, filename):
|
||||
return os.path.join(instance.getSubdir(), filename)
|
||||
|
||||
|
||||
class MetadataMixin(models.Model):
|
||||
"""Model mixin class which adds a JSON metadata field to a model, for use by any (and all) plugins.
|
||||
|
||||
The intent of this mixin is to provide a metadata field on a model instance,
|
||||
for plugins to read / modify as required, to store any extra information.
|
||||
|
||||
The assumptions for models implementing this mixin are:
|
||||
|
||||
- The internal InvenTree business logic will make no use of this field
|
||||
- Multiple plugins may read / write to this metadata field, and not assume they have sole rights
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Meta for MetadataMixin."""
|
||||
abstract = True
|
||||
|
||||
metadata = models.JSONField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Plugin Metadata'),
|
||||
help_text=_('JSON metadata field, for use by external plugins'),
|
||||
)
|
||||
|
||||
def get_metadata(self, key: str, backup_value=None):
|
||||
"""Finds metadata for this model instance, using the provided key for lookup.
|
||||
|
||||
Args:
|
||||
key: String key for requesting metadata. e.g. if a plugin is accessing the metadata, the plugin slug should be used
|
||||
|
||||
Returns:
|
||||
Python dict object containing requested metadata. If no matching metadata is found, returns None
|
||||
"""
|
||||
if self.metadata is None:
|
||||
return backup_value
|
||||
|
||||
return self.metadata.get(key, backup_value)
|
||||
|
||||
def set_metadata(self, key: str, data, commit: bool = True):
|
||||
"""Save the provided metadata under the provided key.
|
||||
|
||||
Args:
|
||||
key (str): Key for saving metadata
|
||||
data (Any): Data object to save - must be able to be rendered as a JSON string
|
||||
commit (bool, optional): If true, existing metadata with the provided key will be overwritten. If false, a merge will be attempted. Defaults to True.
|
||||
"""
|
||||
if self.metadata is None:
|
||||
# Handle a null field value
|
||||
self.metadata = {}
|
||||
|
||||
self.metadata[key] = data
|
||||
|
||||
if commit:
|
||||
self.save()
|
||||
|
||||
|
||||
class DataImportMixin(object):
|
||||
"""Model mixin class which provides support for 'data import' functionality.
|
||||
|
||||
@ -132,7 +186,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
if cls.REFERENCE_PATTERN_SETTING is None:
|
||||
return ''
|
||||
|
||||
return InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
|
||||
return common.models.InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
|
||||
|
||||
@classmethod
|
||||
def get_reference_context(cls):
|
||||
@ -411,7 +465,7 @@ class InvenTreeAttachment(models.Model):
|
||||
blank=True, null=True
|
||||
)
|
||||
|
||||
link = InvenTreeURLField(
|
||||
link = InvenTree.fields.InvenTreeURLField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Link'),
|
||||
help_text=_('Link to external URL')
|
||||
@ -685,7 +739,7 @@ class InvenTreeNotesMixin(models.Model):
|
||||
"""
|
||||
abstract = True
|
||||
|
||||
notes = InvenTreeNotesField(
|
||||
notes = InvenTree.fields.InvenTreeNotesField(
|
||||
verbose_name=_('Notes'),
|
||||
help_text=_('Markdown notes (optional)'),
|
||||
)
|
||||
|
@ -92,6 +92,14 @@ class IsSuperuser(permissions.IsAdminUser):
|
||||
return bool(request.user and request.user.is_superuser)
|
||||
|
||||
|
||||
class IsStaffOrReadOnly(permissions.IsAdminUser):
|
||||
"""Allows read-only access to any user, but write access is restricted to staff users."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check if the user is a superuser."""
|
||||
return bool(request.user and request.user.is_staff or request.method in permissions.SAFE_METHODS)
|
||||
|
||||
|
||||
def auth_exempt(view_func):
|
||||
"""Mark a view function as being exempt from auth requirements."""
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
@ -22,27 +22,24 @@ from mptt.exceptions import InvalidMove
|
||||
from rest_framework import serializers
|
||||
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
|
||||
from InvenTree.helpers import increment, normalize, notify_responsible
|
||||
from InvenTree.models import InvenTreeAttachment, InvenTreeBarcodeMixin, InvenTreeNotesMixin, ReferenceIndexingMixin
|
||||
|
||||
from build.validators import generate_next_build_reference, validate_build_order_reference
|
||||
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
|
||||
from plugin.events import trigger_event
|
||||
from plugin.models import MetadataMixin
|
||||
|
||||
import common.notifications
|
||||
|
||||
import part.models
|
||||
import stock.models
|
||||
import users.models
|
||||
|
||||
|
||||
class Build(MPTTModel, InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, ReferenceIndexingMixin):
|
||||
class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.InvenTreeNotesMixin, InvenTree.models.MetadataMixin, InvenTree.models.ReferenceIndexingMixin):
|
||||
"""A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
||||
|
||||
Attributes:
|
||||
@ -464,7 +461,7 @@ class Build(MPTTModel, InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin
|
||||
new_ref = ref
|
||||
|
||||
while 1:
|
||||
new_ref = increment(new_ref)
|
||||
new_ref = InvenTree.helpers.increment(new_ref)
|
||||
|
||||
if new_ref in tries:
|
||||
# We are potentially stuck in a loop - simply return the original reference
|
||||
@ -1125,10 +1122,10 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
||||
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
||||
|
||||
# Notify the responsible users that the build order has been created
|
||||
notify_responsible(instance, sender, exclude=instance.issued_by)
|
||||
InvenTree.helpers.notify_responsible(instance, sender, exclude=instance.issued_by)
|
||||
|
||||
|
||||
class BuildOrderAttachment(InvenTreeAttachment):
|
||||
class BuildOrderAttachment(InvenTree.models.InvenTreeAttachment):
|
||||
"""Model for storing file attachments against a BuildOrder object."""
|
||||
|
||||
def getSubdir(self):
|
||||
@ -1138,7 +1135,7 @@ class BuildOrderAttachment(InvenTreeAttachment):
|
||||
build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='attachments')
|
||||
|
||||
|
||||
class BuildItem(MetadataMixin, models.Model):
|
||||
class BuildItem(InvenTree.models.MetadataMixin, models.Model):
|
||||
"""A BuildItem links multiple StockItem objects to a Build.
|
||||
|
||||
These are used to allocate part stock to a build. Once the Build is completed, the parts are removed from stock and the BuildItemAllocation objects are removed.
|
||||
@ -1188,8 +1185,8 @@ class BuildItem(MetadataMixin, models.Model):
|
||||
# Allocated quantity cannot exceed available stock quantity
|
||||
if self.quantity > self.stock_item.quantity:
|
||||
|
||||
q = normalize(self.quantity)
|
||||
a = normalize(self.stock_item.quantity)
|
||||
q = InvenTree.helpers.normalize(self.quantity)
|
||||
a = InvenTree.helpers.normalize(self.stock_item.quantity)
|
||||
|
||||
raise ValidationError({
|
||||
'quantity': _(f'Allocated quantity ({q}) must not exceed available stock quantity ({a})')
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -26,9 +26,8 @@ import InvenTree.validators
|
||||
from common.settings import currency_code_default
|
||||
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||
InvenTreeNotesMixin)
|
||||
InvenTreeNotesMixin, MetadataMixin)
|
||||
from InvenTree.status_codes import PurchaseOrderStatus
|
||||
from plugin.models import MetadataMixin
|
||||
|
||||
|
||||
def rename_company_image(instance, filename):
|
||||
|
@ -17,7 +17,7 @@ import common.models
|
||||
import part.models
|
||||
import stock.models
|
||||
from InvenTree.helpers import normalize, validateFilterString
|
||||
from plugin.models import MetadataMixin
|
||||
from InvenTree.models import MetadataMixin
|
||||
|
||||
try:
|
||||
from django_weasyprint import WeasyTemplateResponseMixin
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Admin functionality for the 'order' app"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import import_export.widgets as widgets
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
@ -10,6 +11,19 @@ import order.models as models
|
||||
from InvenTree.admin import InvenTreeResource
|
||||
|
||||
|
||||
class ProjectCodeResourceMixin:
|
||||
"""Mixin for exporting project code data"""
|
||||
|
||||
project_code = Field(attribute='project_code', column_name=_('Project Code'))
|
||||
|
||||
def dehydrate_project_code(self, order):
|
||||
"""Return the project code value, not the pk"""
|
||||
if order.project_code:
|
||||
return order.project_code.code
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
# region general classes
|
||||
class GeneralExtraLineAdmin:
|
||||
"""Admin class template for the 'ExtraLineItem' models"""
|
||||
@ -94,7 +108,7 @@ class SalesOrderAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('customer',)
|
||||
|
||||
|
||||
class PurchaseOrderResource(InvenTreeResource):
|
||||
class PurchaseOrderResource(ProjectCodeResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import / export of PurchaseOrder data."""
|
||||
|
||||
class Meta:
|
||||
@ -141,7 +155,7 @@ class PurchaseOrderExtraLineResource(InvenTreeResource):
|
||||
model = models.PurchaseOrderExtraLine
|
||||
|
||||
|
||||
class SalesOrderResource(InvenTreeResource):
|
||||
class SalesOrderResource(ProjectCodeResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import / export of SalesOrder data."""
|
||||
|
||||
class Meta:
|
||||
@ -276,7 +290,7 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('line', 'shipment', 'item',)
|
||||
|
||||
|
||||
class ReturnOrderResource(InvenTreeResource):
|
||||
class ReturnOrderResource(ProjectCodeResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import / export of ReturnOrder data"""
|
||||
|
||||
class Meta:
|
||||
|
@ -16,7 +16,7 @@ from rest_framework.response import Response
|
||||
|
||||
import order.models as models
|
||||
import order.serializers as serializers
|
||||
from common.models import InvenTreeSetting
|
||||
from common.models import InvenTreeSetting, ProjectCode
|
||||
from common.settings import settings
|
||||
from company.models import SupplierPart
|
||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||
@ -136,6 +136,21 @@ class OrderFilter(rest_filters.FilterSet):
|
||||
else:
|
||||
return queryset.exclude(status__in=self.Meta.model.get_status_class().OPEN)
|
||||
|
||||
project_code = rest_filters.ModelChoiceFilter(
|
||||
queryset=ProjectCode.objects.all(),
|
||||
field_name='project_code'
|
||||
)
|
||||
|
||||
has_project_code = rest_filters.BooleanFilter(label='has_project_code', method='filter_has_project_code')
|
||||
|
||||
def filter_has_project_code(self, queryset, name, value):
|
||||
"""Filter by whether or not the order has a project code"""
|
||||
|
||||
if str2bool(value):
|
||||
return queryset.exclude(project_code=None)
|
||||
else:
|
||||
return queryset.filter(project_code=None)
|
||||
|
||||
|
||||
class LineItemFilter(rest_filters.FilterSet):
|
||||
"""Base class for custom API filters for order line item list(s)"""
|
||||
@ -307,12 +322,14 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
|
||||
ordering_field_aliases = {
|
||||
'reference': ['reference_int', 'reference'],
|
||||
'project_code': ['project_code__code'],
|
||||
}
|
||||
|
||||
search_fields = [
|
||||
'reference',
|
||||
'supplier__name',
|
||||
'supplier_reference',
|
||||
'project_code__code',
|
||||
'description',
|
||||
]
|
||||
|
||||
@ -325,6 +342,7 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
'status',
|
||||
'responsible',
|
||||
'total_price',
|
||||
'project_code',
|
||||
]
|
||||
|
||||
ordering = '-reference'
|
||||
@ -685,6 +703,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
|
||||
ordering_field_aliases = {
|
||||
'reference': ['reference_int', 'reference'],
|
||||
'project_code': ['project_code__code'],
|
||||
}
|
||||
|
||||
filterset_fields = [
|
||||
@ -701,6 +720,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
'line_items',
|
||||
'shipment_date',
|
||||
'total_price',
|
||||
'project_code',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
@ -708,6 +728,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
'reference',
|
||||
'description',
|
||||
'customer_reference',
|
||||
'project_code__code',
|
||||
]
|
||||
|
||||
ordering = '-reference'
|
||||
@ -1138,6 +1159,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
|
||||
ordering_field_aliases = {
|
||||
'reference': ['reference_int', 'reference'],
|
||||
'project_code': ['project_code__code'],
|
||||
}
|
||||
|
||||
ordering_fields = [
|
||||
@ -1148,6 +1170,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
'line_items',
|
||||
'status',
|
||||
'target_date',
|
||||
'project_code',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
@ -1155,6 +1178,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
||||
'reference',
|
||||
'description',
|
||||
'customer_reference',
|
||||
'project_code__code',
|
||||
]
|
||||
|
||||
ordering = '-reference'
|
||||
|
30
InvenTree/order/migrations/0092_auto_20230419_0250.py
Normal file
30
InvenTree/order/migrations/0092_auto_20230419_0250.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.2.18 on 2023-04-19 02:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0018_projectcode'),
|
||||
('order', '0091_auto_20230419_0037'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchaseorder',
|
||||
name='project_code',
|
||||
field=models.ForeignKey(blank=True, help_text='Select project code for this order', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.projectcode', verbose_name='Project Code'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='returnorder',
|
||||
name='project_code',
|
||||
field=models.ForeignKey(blank=True, help_text='Select project code for this order', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.projectcode', verbose_name='Project Code'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='salesorder',
|
||||
name='project_code',
|
||||
field=models.ForeignKey(blank=True, help_text='Select project code for this order', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.projectcode', verbose_name='Project Code'),
|
||||
),
|
||||
]
|
@ -28,6 +28,7 @@ import InvenTree.tasks
|
||||
import order.validators
|
||||
import stock.models
|
||||
import users.models as UserModels
|
||||
from common.models import ProjectCode
|
||||
from common.notifications import InvenTreeNotificationBodies
|
||||
from common.settings import currency_code_default
|
||||
from company.models import Company, Contact, SupplierPart
|
||||
@ -36,13 +37,13 @@ from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeURLField,
|
||||
RoundingDecimalField)
|
||||
from InvenTree.helpers import decimal2string, getSetting, notify_responsible
|
||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||
InvenTreeNotesMixin, ReferenceIndexingMixin)
|
||||
InvenTreeNotesMixin, MetadataMixin,
|
||||
ReferenceIndexingMixin)
|
||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
||||
ReturnOrderStatus, SalesOrderStatus,
|
||||
StockHistoryCode, StockStatus)
|
||||
from part import models as PartModels
|
||||
from plugin.events import trigger_event
|
||||
from plugin.models import MetadataMixin
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
@ -199,6 +200,8 @@ class Order(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, Reference
|
||||
|
||||
description = models.CharField(max_length=250, blank=True, verbose_name=_('Description'), help_text=_('Order description (optional)'))
|
||||
|
||||
project_code = models.ForeignKey(ProjectCode, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Project Code'), help_text=_('Select project code for this order'))
|
||||
|
||||
link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page'))
|
||||
|
||||
target_date = models.DateField(
|
||||
|
@ -13,6 +13,7 @@ from rest_framework import serializers
|
||||
from rest_framework.serializers import ValidationError
|
||||
from sql_util.utils import SubqueryCount
|
||||
|
||||
import common.serializers
|
||||
import order.models
|
||||
import part.filters
|
||||
import stock.models
|
||||
@ -64,6 +65,9 @@ class AbstractOrderSerializer(serializers.Serializer):
|
||||
# Detail for responsible field
|
||||
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
|
||||
|
||||
# Detail for project code field
|
||||
project_code_detail = common.serializers.ProjectCodeSerializer(source='project_code', read_only=True, many=False)
|
||||
|
||||
# Boolean field indicating if this order is overdue (Note: must be annotated)
|
||||
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
@ -96,6 +100,8 @@ class AbstractOrderSerializer(serializers.Serializer):
|
||||
'description',
|
||||
'line_items',
|
||||
'link',
|
||||
'project_code',
|
||||
'project_code_detail',
|
||||
'reference',
|
||||
'responsible',
|
||||
'responsible_detail',
|
||||
|
@ -115,6 +115,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{% trans "Order Description" %}</td>
|
||||
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||
</tr>
|
||||
{% include "project_code_data.html" with instance=order %}
|
||||
{% include "barcode_data.html" with instance=order %}
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
|
@ -107,9 +107,8 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{% trans "Order Description" %}</td>
|
||||
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||
</tr>
|
||||
|
||||
{% include "project_code_data.html" with instance=order %}
|
||||
{% include "barcode_data.html" with instance=order %}
|
||||
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Order Status" %}</td>
|
||||
|
@ -112,6 +112,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{% trans "Order Description" %}</td>
|
||||
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||
</tr>
|
||||
{% include "project_code_data.html" with instance=order %}
|
||||
{% include "barcode_data.html" with instance=order %}
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
|
@ -47,11 +47,10 @@ from InvenTree.fields import InvenTreeURLField
|
||||
from InvenTree.helpers import decimal2money, decimal2string, normalize
|
||||
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
||||
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
||||
InvenTreeTree)
|
||||
InvenTreeTree, MetadataMixin)
|
||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
||||
SalesOrderStatus)
|
||||
from order import models as OrderModels
|
||||
from plugin.models import MetadataMixin
|
||||
from stock import models as StockModels
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
@ -12,60 +12,6 @@ import common.models
|
||||
from plugin import InvenTreePlugin, registry
|
||||
|
||||
|
||||
class MetadataMixin(models.Model):
|
||||
"""Model mixin class which adds a JSON metadata field to a model, for use by any (and all) plugins.
|
||||
|
||||
The intent of this mixin is to provide a metadata field on a model instance,
|
||||
for plugins to read / modify as required, to store any extra information.
|
||||
|
||||
The assumptions for models implementing this mixin are:
|
||||
|
||||
- The internal InvenTree business logic will make no use of this field
|
||||
- Multiple plugins may read / write to this metadata field, and not assume they have sole rights
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Meta for MetadataMixin."""
|
||||
abstract = True
|
||||
|
||||
metadata = models.JSONField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Plugin Metadata'),
|
||||
help_text=_('JSON metadata field, for use by external plugins'),
|
||||
)
|
||||
|
||||
def get_metadata(self, key: str, backup_value=None):
|
||||
"""Finds metadata for this model instance, using the provided key for lookup.
|
||||
|
||||
Args:
|
||||
key: String key for requesting metadata. e.g. if a plugin is accessing the metadata, the plugin slug should be used
|
||||
|
||||
Returns:
|
||||
Python dict object containing requested metadata. If no matching metadata is found, returns None
|
||||
"""
|
||||
if self.metadata is None:
|
||||
return backup_value
|
||||
|
||||
return self.metadata.get(key, backup_value)
|
||||
|
||||
def set_metadata(self, key: str, data, commit: bool = True):
|
||||
"""Save the provided metadata under the provided key.
|
||||
|
||||
Args:
|
||||
key (str): Key for saving metadata
|
||||
data (Any): Data object to save - must be able to be rendered as a JSON string
|
||||
commit (bool, optional): If true, existing metadata with the provided key will be overwritten. If false, a merge will be attempted. Defaults to True.
|
||||
"""
|
||||
if self.metadata is None:
|
||||
# Handle a null field value
|
||||
self.metadata = {}
|
||||
|
||||
self.metadata[key] = data
|
||||
|
||||
if commit:
|
||||
self.save()
|
||||
|
||||
|
||||
class PluginConfig(models.Model):
|
||||
"""A PluginConfig object holds settings for plugins.
|
||||
|
||||
|
@ -21,7 +21,7 @@ import order.models
|
||||
import part.models
|
||||
import stock.models
|
||||
from InvenTree.helpers import validateFilterString
|
||||
from plugin.models import MetadataMixin
|
||||
from InvenTree.models import MetadataMixin
|
||||
|
||||
try:
|
||||
from django_weasyprint import WeasyTemplateResponseMixin
|
||||
|
@ -31,12 +31,12 @@ import report.models
|
||||
from company import models as CompanyModels
|
||||
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||
InvenTreeNotesMixin, InvenTreeTree, extract_int)
|
||||
InvenTreeNotesMixin, InvenTreeTree,
|
||||
MetadataMixin, extract_int)
|
||||
from InvenTree.status_codes import (SalesOrderStatus, StockHistoryCode,
|
||||
StockStatus)
|
||||
from part import models as PartModels
|
||||
from plugin.events import trigger_event
|
||||
from plugin.models import MetadataMixin
|
||||
from users.models import Owner
|
||||
|
||||
|
||||
|
35
InvenTree/templates/InvenTree/settings/project_codes.html
Normal file
35
InvenTree/templates/InvenTree/settings/project_codes.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends "panel.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block label %}project-codes{% endblock label %}
|
||||
|
||||
{% block heading %}{% trans "Project Code Settings" %}{% endblock heading %}
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Project code settings -->
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="PROJECT_CODES_ENABLED" icon='fa-toggle-on' %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-span'>
|
||||
<h4>{% trans "Project Codes" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
<div class='btn-group' role='group'>
|
||||
<button class='btn btn-success' id='new-project-code'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Project Code" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='project-code-table'>
|
||||
</table>
|
||||
{% endblock content %}
|
@ -31,6 +31,7 @@
|
||||
{% include "InvenTree/settings/global.html" %}
|
||||
{% include "InvenTree/settings/login.html" %}
|
||||
{% include "InvenTree/settings/barcode.html" %}
|
||||
{% include "InvenTree/settings/project_codes.html" %}
|
||||
{% include "InvenTree/settings/notifications.html" %}
|
||||
{% include "InvenTree/settings/label.html" %}
|
||||
{% include "InvenTree/settings/report.html" %}
|
||||
|
@ -52,6 +52,78 @@ onPanelLoad('pricing', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Javascript for project codes panel
|
||||
onPanelLoad('project-codes', function() {
|
||||
|
||||
// Construct the project code table
|
||||
$('#project-code-table').bootstrapTable({
|
||||
url: '{% url "api-project-code-list" %}',
|
||||
search: true,
|
||||
sortable: true,
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No project codes found" %}';
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'code',
|
||||
sortable: true,
|
||||
title: '{% trans "Project Code" %}',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
sortable: false,
|
||||
title: '{% trans "Description" %}',
|
||||
formatter: function(value, row) {
|
||||
let html = value;
|
||||
let buttons = '';
|
||||
|
||||
buttons += makeEditButton('button-project-code-edit', row.pk, '{% trans "Edit Project Code" %}');
|
||||
buttons += makeDeleteButton('button-project-code-delete', row.pk, '{% trans "Delete Project Code" %}');
|
||||
|
||||
html += wrapButtons(buttons);
|
||||
return html;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$('#project-code-table').on('click', '.button-project-code-edit', function() {
|
||||
let pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`{% url "api-project-code-list" %}${pk}/`, {
|
||||
title: '{% trans "Edit Project Code" %}',
|
||||
fields: {
|
||||
code: {},
|
||||
description: {},
|
||||
},
|
||||
refreshTable: '#project-code-table',
|
||||
});
|
||||
});
|
||||
|
||||
$('#project-code-table').on('click', '.button-project-code-delete', function() {
|
||||
let pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`{% url "api-project-code-list" %}${pk}/`, {
|
||||
title: '{% trans "Delete Project Code" %}',
|
||||
method: 'DELETE',
|
||||
refreshTable: '#project-code-table',
|
||||
});
|
||||
});
|
||||
|
||||
$('#new-project-code').click(function() {
|
||||
// Construct a new project code
|
||||
constructForm('{% url "api-project-code-list" %}', {
|
||||
fields: {
|
||||
code: {},
|
||||
description: {},
|
||||
},
|
||||
title: '{% trans "New Project Code" %}',
|
||||
method: 'POST',
|
||||
refreshTable: '#project-code-table',
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Javascript for Part Category panel
|
||||
onPanelLoad('category', function() {
|
||||
$('#category-select').select2({
|
||||
@ -136,11 +208,12 @@ onPanelLoad('category', function() {
|
||||
title: '{% trans "Default Value" %}',
|
||||
sortable: 'true',
|
||||
formatter: function(value, row, index, field) {
|
||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||
let buttons = '';
|
||||
buttons += makeEditButton('template-edit', row.pk, '{% trans "Edit Template" %}');
|
||||
buttons += makeDeleteButton('template-delete', row.pk, '{% trans "Delete Template" %}');
|
||||
|
||||
var html = value
|
||||
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||
let html = value
|
||||
html += wrapButtons(buttons);
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -154,6 +227,7 @@ onPanelLoad('category', function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/part/category/parameters/${pk}/`, {
|
||||
title: '{% trans "Edit Category Parameter Template" %}',
|
||||
fields: {
|
||||
parameter_template: {},
|
||||
category: {
|
||||
|
@ -30,6 +30,8 @@
|
||||
{% include "sidebar_item.html" with label='login' text=text icon="fa-fingerprint" %}
|
||||
{% trans "Barcode Support" as text %}
|
||||
{% include "sidebar_item.html" with label='barcodes' text=text icon="fa-qrcode" %}
|
||||
{% trans "Project Codes" as text %}
|
||||
{% include "sidebar_item.html" with label='project-codes' text=text icon="fa-list" %}
|
||||
{% trans "Notifications" as text %}
|
||||
{% include "sidebar_item.html" with label='global-notifications' text=text icon="fa-bell" %}
|
||||
{% trans "Pricing" as text %}
|
||||
|
@ -1123,9 +1123,9 @@ function loadSupplierPartTable(table, url, options) {
|
||||
var params = options.params || {};
|
||||
|
||||
// Load filters
|
||||
var filters = loadTableFilters('supplier-part', params);
|
||||
var filters = loadTableFilters('supplierpart', params);
|
||||
|
||||
setupFilterList('supplier-part', $(table));
|
||||
setupFilterList('supplierpart', $(table));
|
||||
|
||||
$(table).inventreeTable({
|
||||
url: url,
|
||||
|
@ -16,6 +16,7 @@
|
||||
renderOwner,
|
||||
renderPart,
|
||||
renderPartCategory,
|
||||
renderProjectCode,
|
||||
renderReturnOrder,
|
||||
renderStockItem,
|
||||
renderStockLocation,
|
||||
@ -78,6 +79,8 @@ function getModelRenderer(model) {
|
||||
return renderUser;
|
||||
case 'group':
|
||||
return renderGroup;
|
||||
case 'projectcode':
|
||||
return renderProjectCode;
|
||||
default:
|
||||
// Un-handled model type
|
||||
console.error(`Rendering not implemented for model '${model}'`);
|
||||
@ -476,3 +479,16 @@ function renderSupplierPart(data, parameters={}) {
|
||||
parameters
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Renderer for "ProjectCode" model
|
||||
function renderProjectCode(data, parameters={}) {
|
||||
|
||||
return renderModel(
|
||||
{
|
||||
text: data.code,
|
||||
textSecondary: data.description,
|
||||
},
|
||||
parameters
|
||||
);
|
||||
}
|
||||
|
@ -62,6 +62,9 @@ function purchaseOrderFields(options={}) {
|
||||
}
|
||||
},
|
||||
supplier_reference: {},
|
||||
project_code: {
|
||||
icon: 'fa-list',
|
||||
},
|
||||
target_date: {
|
||||
icon: 'fa-calendar-alt',
|
||||
},
|
||||
@ -126,6 +129,10 @@ function purchaseOrderFields(options={}) {
|
||||
};
|
||||
}
|
||||
|
||||
if (!global_settings.PROJECT_CODES_ENABLED) {
|
||||
delete fields.project_code;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@ -1614,6 +1621,18 @@ function loadPurchaseOrderTable(table, options) {
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
},
|
||||
{
|
||||
field: 'project_code',
|
||||
title: '{% trans "Project Code" %}',
|
||||
switchable: global_settings.PROJECT_CODES_ENABLED,
|
||||
visible: global_settings.PROJECT_CODES_ENABLED,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
if (row.project_code_detail) {
|
||||
return `<span title='${row.project_code_detail.description}'>${row.project_code_detail.code}</span>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '{% trans "Status" %}',
|
||||
|
@ -46,6 +46,9 @@ function returnOrderFields(options={}) {
|
||||
}
|
||||
},
|
||||
customer_reference: {},
|
||||
project_code: {
|
||||
icon: 'fa-list',
|
||||
},
|
||||
target_date: {
|
||||
icon: 'fa-calendar-alt',
|
||||
},
|
||||
@ -69,6 +72,10 @@ function returnOrderFields(options={}) {
|
||||
}
|
||||
};
|
||||
|
||||
if (!global_settings.PROJECT_CODES_ENABLED) {
|
||||
delete fields.project_code;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@ -271,6 +278,18 @@ function loadReturnOrderTable(table, options={}) {
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
},
|
||||
{
|
||||
field: 'project_code',
|
||||
title: '{% trans "Project Code" %}',
|
||||
switchable: global_settings.PROJECT_CODES_ENABLED,
|
||||
visible: global_settings.PROJECT_CODES_ENABLED,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
if (row.project_code_detail) {
|
||||
return `<span title='${row.project_code_detail.description}'>${row.project_code_detail.code}</span>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'status',
|
||||
|
@ -59,6 +59,9 @@ function salesOrderFields(options={}) {
|
||||
}
|
||||
},
|
||||
customer_reference: {},
|
||||
project_code: {
|
||||
icon: 'fa-list',
|
||||
},
|
||||
target_date: {
|
||||
icon: 'fa-calendar-alt',
|
||||
},
|
||||
@ -82,6 +85,10 @@ function salesOrderFields(options={}) {
|
||||
}
|
||||
};
|
||||
|
||||
if (!global_settings.PROJECT_CODES_ENABLED) {
|
||||
delete fields.project_code;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@ -739,6 +746,18 @@ function loadSalesOrderTable(table, options) {
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
},
|
||||
{
|
||||
field: 'project_code',
|
||||
title: '{% trans "Project Code" %}',
|
||||
switchable: global_settings.PROJECT_CODES_ENABLED,
|
||||
visible: global_settings.PROJECT_CODES_ENABLED,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
if (row.project_code_detail) {
|
||||
return `<span title='${row.project_code_detail.description}'>${row.project_code_detail.code}</span>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'status',
|
||||
|
File diff suppressed because it is too large
Load Diff
11
InvenTree/templates/project_code_data.html
Normal file
11
InvenTree/templates/project_code_data.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if instance and instance.project_code %}
|
||||
<tr>
|
||||
<td><span class='fas fa-list'></span></td>
|
||||
<td>{% trans "Project Code" %}</td>
|
||||
<td>
|
||||
{{ instance.project_code.code }} - <em><small>{{ instance.project_code.description }}</small></em>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
@ -181,11 +181,12 @@ class RuleSet(models.Model):
|
||||
'common_colortheme',
|
||||
'common_inventreesetting',
|
||||
'common_inventreeusersetting',
|
||||
'common_webhookendpoint',
|
||||
'common_webhookmessage',
|
||||
'common_notificationentry',
|
||||
'common_notificationmessage',
|
||||
'common_notesimage',
|
||||
'common_projectcode',
|
||||
'common_webhookendpoint',
|
||||
'common_webhookmessage',
|
||||
'users_owner',
|
||||
|
||||
# Third-party tables
|
||||
|
Loading…
Reference in New Issue
Block a user