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
|
||||||
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
|
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
|
v108 -> 2023-04-17 : https://github.com/inventree/InvenTree/pull/4615
|
||||||
- Adds functionality to upload images for rendering in markdown notes
|
- 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 djmoney.models.validators import MinMoneyValidator
|
||||||
from rest_framework.fields import URLField as RestURLField
|
from rest_framework.fields import URLField as RestURLField
|
||||||
|
|
||||||
import InvenTree.helpers
|
|
||||||
|
|
||||||
from .validators import AllowedURLValidator, allowable_url_schemes
|
from .validators import AllowedURLValidator, allowable_url_schemes
|
||||||
|
|
||||||
|
|
||||||
@ -150,6 +148,8 @@ class DatePickerFormField(forms.DateField):
|
|||||||
def round_decimal(value, places, normalize=False):
|
def round_decimal(value, places, normalize=False):
|
||||||
"""Round value to the specified number of places."""
|
"""Round value to the specified number of places."""
|
||||||
|
|
||||||
|
import InvenTree.helpers
|
||||||
|
|
||||||
if type(value) in [Decimal, float]:
|
if type(value) in [Decimal, float]:
|
||||||
value = round(value, places)
|
value = round(value, places)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool
|
import InvenTree.helpers
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeSearchFilter(filters.SearchFilter):
|
class InvenTreeSearchFilter(filters.SearchFilter):
|
||||||
@ -16,7 +16,7 @@ class InvenTreeSearchFilter(filters.SearchFilter):
|
|||||||
- search_regex: If True, search is perfomed on 'regex' comparison
|
- 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)
|
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
|
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 = []
|
terms = []
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ from djmoney.contrib.exchange.models import convert_money
|
|||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
import common.models
|
||||||
import InvenTree.version
|
import InvenTree.version
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
from common.notifications import (InvenTreeNotificationBodies,
|
from common.notifications import (InvenTreeNotificationBodies,
|
||||||
NotificationBody, trigger_notification)
|
NotificationBody, trigger_notification)
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
@ -43,7 +43,7 @@ logger = logging.getLogger('inventree')
|
|||||||
|
|
||||||
def getSetting(key, backup_value=None):
|
def getSetting(key, backup_value=None):
|
||||||
"""Shortcut for reading a setting value from the database."""
|
"""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):
|
def generateTestKey(test_name):
|
||||||
@ -96,7 +96,7 @@ def construct_absolute_url(*arg):
|
|||||||
|
|
||||||
This requires the BASE_URL configuration option to be set!
|
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)
|
url = '/'.join(arg)
|
||||||
|
|
||||||
@ -145,10 +145,10 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||||||
validator(remote_url)
|
validator(remote_url)
|
||||||
|
|
||||||
# Calculate maximum allowable image size (in bytes)
|
# 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)
|
# 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:
|
if user_agent:
|
||||||
headers = {"User-Agent": user_agent}
|
headers = {"User-Agent": user_agent}
|
||||||
else:
|
else:
|
||||||
@ -1138,10 +1138,10 @@ def render_currency(money, decimal_places=None, currency=None, include_symbol=Tr
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if decimal_places is None:
|
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:
|
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 = Decimal(str(money.amount)).normalize()
|
||||||
value = str(value)
|
value = str(value)
|
||||||
|
@ -21,10 +21,10 @@ from error_report.models import Error
|
|||||||
from mptt.exceptions import InvalidMove
|
from mptt.exceptions import InvalidMove
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
|
import common.models
|
||||||
|
import InvenTree.fields
|
||||||
import InvenTree.format
|
import InvenTree.format
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
from InvenTree.fields import InvenTreeNotesField, InvenTreeURLField
|
|
||||||
from InvenTree.sanitizer import sanitize_svg
|
from InvenTree.sanitizer import sanitize_svg
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
@ -44,6 +44,60 @@ def rename_attachment(instance, filename):
|
|||||||
return os.path.join(instance.getSubdir(), 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):
|
class DataImportMixin(object):
|
||||||
"""Model mixin class which provides support for 'data import' functionality.
|
"""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:
|
if cls.REFERENCE_PATTERN_SETTING is None:
|
||||||
return ''
|
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
|
@classmethod
|
||||||
def get_reference_context(cls):
|
def get_reference_context(cls):
|
||||||
@ -411,7 +465,7 @@ class InvenTreeAttachment(models.Model):
|
|||||||
blank=True, null=True
|
blank=True, null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
link = InvenTreeURLField(
|
link = InvenTree.fields.InvenTreeURLField(
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
verbose_name=_('Link'),
|
verbose_name=_('Link'),
|
||||||
help_text=_('Link to external URL')
|
help_text=_('Link to external URL')
|
||||||
@ -685,7 +739,7 @@ class InvenTreeNotesMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
notes = InvenTreeNotesField(
|
notes = InvenTree.fields.InvenTreeNotesField(
|
||||||
verbose_name=_('Notes'),
|
verbose_name=_('Notes'),
|
||||||
help_text=_('Markdown notes (optional)'),
|
help_text=_('Markdown notes (optional)'),
|
||||||
)
|
)
|
||||||
|
@ -92,6 +92,14 @@ class IsSuperuser(permissions.IsAdminUser):
|
|||||||
return bool(request.user and request.user.is_superuser)
|
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):
|
def auth_exempt(view_func):
|
||||||
"""Mark a view function as being exempt from auth requirements."""
|
"""Mark a view function as being exempt from auth requirements."""
|
||||||
def wrapped_view(*args, **kwargs):
|
def wrapped_view(*args, **kwargs):
|
||||||
|
@ -22,27 +22,24 @@ from mptt.exceptions import InvalidMove
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
|
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
|
from build.validators import generate_next_build_reference, validate_build_order_reference
|
||||||
|
|
||||||
import InvenTree.fields
|
import InvenTree.fields
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.models
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
from plugin.models import MetadataMixin
|
|
||||||
|
|
||||||
import common.notifications
|
import common.notifications
|
||||||
|
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
import users.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.
|
"""A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@ -464,7 +461,7 @@ class Build(MPTTModel, InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin
|
|||||||
new_ref = ref
|
new_ref = ref
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
new_ref = increment(new_ref)
|
new_ref = InvenTree.helpers.increment(new_ref)
|
||||||
|
|
||||||
if new_ref in tries:
|
if new_ref in tries:
|
||||||
# We are potentially stuck in a loop - simply return the original reference
|
# 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)
|
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
||||||
|
|
||||||
# Notify the responsible users that the build order has been created
|
# 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."""
|
"""Model for storing file attachments against a BuildOrder object."""
|
||||||
|
|
||||||
def getSubdir(self):
|
def getSubdir(self):
|
||||||
@ -1138,7 +1135,7 @@ class BuildOrderAttachment(InvenTreeAttachment):
|
|||||||
build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='attachments')
|
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.
|
"""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.
|
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
|
# Allocated quantity cannot exceed available stock quantity
|
||||||
if self.quantity > self.stock_item.quantity:
|
if self.quantity > self.stock_item.quantity:
|
||||||
|
|
||||||
q = normalize(self.quantity)
|
q = InvenTree.helpers.normalize(self.quantity)
|
||||||
a = normalize(self.stock_item.quantity)
|
a = InvenTree.helpers.normalize(self.stock_item.quantity)
|
||||||
|
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'quantity': _(f'Allocated quantity ({q}) must not exceed available stock quantity ({a})')
|
'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.models
|
||||||
import common.serializers
|
import common.serializers
|
||||||
from InvenTree.api import BulkDeleteMixin
|
from InvenTree.api import BulkDeleteMixin, MetadataView
|
||||||
from InvenTree.config import CONFIG_LOOKUPS
|
from InvenTree.config import CONFIG_LOOKUPS
|
||||||
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
|
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
|
||||||
from InvenTree.helpers import inheritors
|
from InvenTree.helpers import inheritors
|
||||||
from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI,
|
from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI,
|
||||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||||
from InvenTree.permissions import IsSuperuser
|
from InvenTree.permissions import IsStaffOrReadOnly, IsSuperuser
|
||||||
from plugin.models import NotificationUserSetting
|
from plugin.models import NotificationUserSetting
|
||||||
from plugin.serializers import NotificationUserSettingSerializer
|
from plugin.serializers import NotificationUserSettingSerializer
|
||||||
|
|
||||||
@ -454,6 +454,22 @@ class NotesImageList(ListCreateAPI):
|
|||||||
image.save()
|
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 = [
|
settings_api_urls = [
|
||||||
# User settings
|
# User settings
|
||||||
re_path(r'^user/', include([
|
re_path(r'^user/', include([
|
||||||
@ -490,6 +506,15 @@ common_api_urls = [
|
|||||||
# Uploaded images for notes
|
# Uploaded images for notes
|
||||||
re_path(r'^notes-image-upload/', NotesImageList.as_view(), name='api-notes-image-list'),
|
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
|
# Currencies
|
||||||
re_path(r'^currency/', include([
|
re_path(r'^currency/', include([
|
||||||
re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'),
|
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 build.validators
|
||||||
import InvenTree.fields
|
import InvenTree.fields
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.models
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
import InvenTree.validators
|
import InvenTree.validators
|
||||||
@ -84,6 +85,33 @@ class EmptyURLValidator(URLValidator):
|
|||||||
super().__call__(value)
|
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):
|
class BaseInvenTreeSetting(models.Model):
|
||||||
"""An base InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values)."""
|
"""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,
|
'requires_restart': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"PROJECT_CODES_ENABLED": {
|
||||||
|
'name': _('Enable project codes'),
|
||||||
|
'description': _('Enable project codes for tracking projects'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
'STOCKTAKE_ENABLE': {
|
'STOCKTAKE_ENABLE': {
|
||||||
'name': _('Stocktake Functionality'),
|
'name': _('Stocktake Functionality'),
|
||||||
'description': _('Enable stocktake functionality for recording stock levels and calculating stock value'),
|
'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 rest_framework import serializers
|
||||||
|
|
||||||
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
||||||
NewsFeedEntry, NotesImage, NotificationMessage)
|
NewsFeedEntry, NotesImage, NotificationMessage,
|
||||||
|
ProjectCode)
|
||||||
from InvenTree.helpers import construct_absolute_url, get_objectreference
|
from InvenTree.helpers import construct_absolute_url, get_objectreference
|
||||||
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
||||||
InvenTreeModelSerializer)
|
InvenTreeModelSerializer)
|
||||||
@ -253,3 +254,17 @@ class NotesImageSerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
image = InvenTreeImageSerializerField(required=True)
|
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 .api import WebhookView
|
||||||
from .models import (ColorTheme, InvenTreeSetting, InvenTreeUserSetting,
|
from .models import (ColorTheme, InvenTreeSetting, InvenTreeUserSetting,
|
||||||
NotesImage, NotificationEntry, NotificationMessage,
|
NotesImage, NotificationEntry, NotificationMessage,
|
||||||
WebhookEndpoint, WebhookMessage)
|
ProjectCode, WebhookEndpoint, WebhookMessage)
|
||||||
|
|
||||||
CONTENT_TYPE_JSON = 'application/json'
|
CONTENT_TYPE_JSON = 'application/json'
|
||||||
|
|
||||||
@ -1001,3 +1001,113 @@ class NotesImageTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
# Check that a new file has been created
|
# Check that a new file has been created
|
||||||
self.assertEqual(NotesImage.objects.count(), n + 1)
|
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 common.settings import currency_code_default
|
||||||
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
||||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||||
InvenTreeNotesMixin)
|
InvenTreeNotesMixin, MetadataMixin)
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus
|
from InvenTree.status_codes import PurchaseOrderStatus
|
||||||
from plugin.models import MetadataMixin
|
|
||||||
|
|
||||||
|
|
||||||
def rename_company_image(instance, filename):
|
def rename_company_image(instance, filename):
|
||||||
|
@ -17,7 +17,7 @@ import common.models
|
|||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.helpers import normalize, validateFilterString
|
from InvenTree.helpers import normalize, validateFilterString
|
||||||
from plugin.models import MetadataMixin
|
from InvenTree.models import MetadataMixin
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django_weasyprint import WeasyTemplateResponseMixin
|
from django_weasyprint import WeasyTemplateResponseMixin
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Admin functionality for the 'order' app"""
|
"""Admin functionality for the 'order' app"""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import import_export.widgets as widgets
|
import import_export.widgets as widgets
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
@ -10,6 +11,19 @@ import order.models as models
|
|||||||
from InvenTree.admin import InvenTreeResource
|
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
|
# region general classes
|
||||||
class GeneralExtraLineAdmin:
|
class GeneralExtraLineAdmin:
|
||||||
"""Admin class template for the 'ExtraLineItem' models"""
|
"""Admin class template for the 'ExtraLineItem' models"""
|
||||||
@ -94,7 +108,7 @@ class SalesOrderAdmin(ImportExportModelAdmin):
|
|||||||
autocomplete_fields = ('customer',)
|
autocomplete_fields = ('customer',)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderResource(InvenTreeResource):
|
class PurchaseOrderResource(ProjectCodeResourceMixin, InvenTreeResource):
|
||||||
"""Class for managing import / export of PurchaseOrder data."""
|
"""Class for managing import / export of PurchaseOrder data."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -141,7 +155,7 @@ class PurchaseOrderExtraLineResource(InvenTreeResource):
|
|||||||
model = models.PurchaseOrderExtraLine
|
model = models.PurchaseOrderExtraLine
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderResource(InvenTreeResource):
|
class SalesOrderResource(ProjectCodeResourceMixin, InvenTreeResource):
|
||||||
"""Class for managing import / export of SalesOrder data."""
|
"""Class for managing import / export of SalesOrder data."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -276,7 +290,7 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin):
|
|||||||
autocomplete_fields = ('line', 'shipment', 'item',)
|
autocomplete_fields = ('line', 'shipment', 'item',)
|
||||||
|
|
||||||
|
|
||||||
class ReturnOrderResource(InvenTreeResource):
|
class ReturnOrderResource(ProjectCodeResourceMixin, InvenTreeResource):
|
||||||
"""Class for managing import / export of ReturnOrder data"""
|
"""Class for managing import / export of ReturnOrder data"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -16,7 +16,7 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
import order.models as models
|
import order.models as models
|
||||||
import order.serializers as serializers
|
import order.serializers as serializers
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting, ProjectCode
|
||||||
from common.settings import settings
|
from common.settings import settings
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||||
@ -136,6 +136,21 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
else:
|
else:
|
||||||
return queryset.exclude(status__in=self.Meta.model.get_status_class().OPEN)
|
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):
|
class LineItemFilter(rest_filters.FilterSet):
|
||||||
"""Base class for custom API filters for order line item list(s)"""
|
"""Base class for custom API filters for order line item list(s)"""
|
||||||
@ -307,12 +322,14 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
ordering_field_aliases = {
|
ordering_field_aliases = {
|
||||||
'reference': ['reference_int', 'reference'],
|
'reference': ['reference_int', 'reference'],
|
||||||
|
'project_code': ['project_code__code'],
|
||||||
}
|
}
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'reference',
|
'reference',
|
||||||
'supplier__name',
|
'supplier__name',
|
||||||
'supplier_reference',
|
'supplier_reference',
|
||||||
|
'project_code__code',
|
||||||
'description',
|
'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -325,6 +342,7 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
'status',
|
'status',
|
||||||
'responsible',
|
'responsible',
|
||||||
'total_price',
|
'total_price',
|
||||||
|
'project_code',
|
||||||
]
|
]
|
||||||
|
|
||||||
ordering = '-reference'
|
ordering = '-reference'
|
||||||
@ -685,6 +703,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
ordering_field_aliases = {
|
ordering_field_aliases = {
|
||||||
'reference': ['reference_int', 'reference'],
|
'reference': ['reference_int', 'reference'],
|
||||||
|
'project_code': ['project_code__code'],
|
||||||
}
|
}
|
||||||
|
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
@ -701,6 +720,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
'line_items',
|
'line_items',
|
||||||
'shipment_date',
|
'shipment_date',
|
||||||
'total_price',
|
'total_price',
|
||||||
|
'project_code',
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
@ -708,6 +728,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
'reference',
|
'reference',
|
||||||
'description',
|
'description',
|
||||||
'customer_reference',
|
'customer_reference',
|
||||||
|
'project_code__code',
|
||||||
]
|
]
|
||||||
|
|
||||||
ordering = '-reference'
|
ordering = '-reference'
|
||||||
@ -1138,6 +1159,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
ordering_field_aliases = {
|
ordering_field_aliases = {
|
||||||
'reference': ['reference_int', 'reference'],
|
'reference': ['reference_int', 'reference'],
|
||||||
|
'project_code': ['project_code__code'],
|
||||||
}
|
}
|
||||||
|
|
||||||
ordering_fields = [
|
ordering_fields = [
|
||||||
@ -1148,6 +1170,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
'line_items',
|
'line_items',
|
||||||
'status',
|
'status',
|
||||||
'target_date',
|
'target_date',
|
||||||
|
'project_code',
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
@ -1155,6 +1178,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
'reference',
|
'reference',
|
||||||
'description',
|
'description',
|
||||||
'customer_reference',
|
'customer_reference',
|
||||||
|
'project_code__code',
|
||||||
]
|
]
|
||||||
|
|
||||||
ordering = '-reference'
|
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 order.validators
|
||||||
import stock.models
|
import stock.models
|
||||||
import users.models as UserModels
|
import users.models as UserModels
|
||||||
|
from common.models import ProjectCode
|
||||||
from common.notifications import InvenTreeNotificationBodies
|
from common.notifications import InvenTreeNotificationBodies
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
from company.models import Company, Contact, SupplierPart
|
from company.models import Company, Contact, SupplierPart
|
||||||
@ -36,13 +37,13 @@ from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeURLField,
|
|||||||
RoundingDecimalField)
|
RoundingDecimalField)
|
||||||
from InvenTree.helpers import decimal2string, getSetting, notify_responsible
|
from InvenTree.helpers import decimal2string, getSetting, notify_responsible
|
||||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||||
InvenTreeNotesMixin, ReferenceIndexingMixin)
|
InvenTreeNotesMixin, MetadataMixin,
|
||||||
|
ReferenceIndexingMixin)
|
||||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
||||||
ReturnOrderStatus, SalesOrderStatus,
|
ReturnOrderStatus, SalesOrderStatus,
|
||||||
StockHistoryCode, StockStatus)
|
StockHistoryCode, StockStatus)
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
from plugin.models import MetadataMixin
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
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)'))
|
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'))
|
link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page'))
|
||||||
|
|
||||||
target_date = models.DateField(
|
target_date = models.DateField(
|
||||||
|
@ -13,6 +13,7 @@ from rest_framework import serializers
|
|||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from sql_util.utils import SubqueryCount
|
from sql_util.utils import SubqueryCount
|
||||||
|
|
||||||
|
import common.serializers
|
||||||
import order.models
|
import order.models
|
||||||
import part.filters
|
import part.filters
|
||||||
import stock.models
|
import stock.models
|
||||||
@ -64,6 +65,9 @@ class AbstractOrderSerializer(serializers.Serializer):
|
|||||||
# Detail for responsible field
|
# Detail for responsible field
|
||||||
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
|
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)
|
# Boolean field indicating if this order is overdue (Note: must be annotated)
|
||||||
overdue = serializers.BooleanField(required=False, read_only=True)
|
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
@ -96,6 +100,8 @@ class AbstractOrderSerializer(serializers.Serializer):
|
|||||||
'description',
|
'description',
|
||||||
'line_items',
|
'line_items',
|
||||||
'link',
|
'link',
|
||||||
|
'project_code',
|
||||||
|
'project_code_detail',
|
||||||
'reference',
|
'reference',
|
||||||
'responsible',
|
'responsible',
|
||||||
'responsible_detail',
|
'responsible_detail',
|
||||||
|
@ -115,6 +115,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{% trans "Order Description" %}</td>
|
<td>{% trans "Order Description" %}</td>
|
||||||
<td>{{ order.description }}{% include "clip.html" %}</td>
|
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% include "project_code_data.html" with instance=order %}
|
||||||
{% include "barcode_data.html" with instance=order %}
|
{% include "barcode_data.html" with instance=order %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
|
@ -107,9 +107,8 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{% trans "Order Description" %}</td>
|
<td>{% trans "Order Description" %}</td>
|
||||||
<td>{{ order.description }}{% include "clip.html" %}</td>
|
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% include "project_code_data.html" with instance=order %}
|
||||||
{% include "barcode_data.html" with instance=order %}
|
{% include "barcode_data.html" with instance=order %}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Order Status" %}</td>
|
<td>{% trans "Order Status" %}</td>
|
||||||
|
@ -112,6 +112,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{% trans "Order Description" %}</td>
|
<td>{% trans "Order Description" %}</td>
|
||||||
<td>{{ order.description }}{% include "clip.html" %}</td>
|
<td>{{ order.description }}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% include "project_code_data.html" with instance=order %}
|
||||||
{% include "barcode_data.html" with instance=order %}
|
{% include "barcode_data.html" with instance=order %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<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.helpers import decimal2money, decimal2string, normalize
|
||||||
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
||||||
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
||||||
InvenTreeTree)
|
InvenTreeTree, MetadataMixin)
|
||||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
||||||
SalesOrderStatus)
|
SalesOrderStatus)
|
||||||
from order import models as OrderModels
|
from order import models as OrderModels
|
||||||
from plugin.models import MetadataMixin
|
|
||||||
from stock import models as StockModels
|
from stock import models as StockModels
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
|
@ -12,60 +12,6 @@ import common.models
|
|||||||
from plugin import InvenTreePlugin, registry
|
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):
|
class PluginConfig(models.Model):
|
||||||
"""A PluginConfig object holds settings for plugins.
|
"""A PluginConfig object holds settings for plugins.
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import order.models
|
|||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.helpers import validateFilterString
|
from InvenTree.helpers import validateFilterString
|
||||||
from plugin.models import MetadataMixin
|
from InvenTree.models import MetadataMixin
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django_weasyprint import WeasyTemplateResponseMixin
|
from django_weasyprint import WeasyTemplateResponseMixin
|
||||||
|
@ -31,12 +31,12 @@ import report.models
|
|||||||
from company import models as CompanyModels
|
from company import models as CompanyModels
|
||||||
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
|
||||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||||
InvenTreeNotesMixin, InvenTreeTree, extract_int)
|
InvenTreeNotesMixin, InvenTreeTree,
|
||||||
|
MetadataMixin, extract_int)
|
||||||
from InvenTree.status_codes import (SalesOrderStatus, StockHistoryCode,
|
from InvenTree.status_codes import (SalesOrderStatus, StockHistoryCode,
|
||||||
StockStatus)
|
StockStatus)
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
from plugin.models import MetadataMixin
|
|
||||||
from users.models import Owner
|
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/global.html" %}
|
||||||
{% include "InvenTree/settings/login.html" %}
|
{% include "InvenTree/settings/login.html" %}
|
||||||
{% include "InvenTree/settings/barcode.html" %}
|
{% include "InvenTree/settings/barcode.html" %}
|
||||||
|
{% include "InvenTree/settings/project_codes.html" %}
|
||||||
{% include "InvenTree/settings/notifications.html" %}
|
{% include "InvenTree/settings/notifications.html" %}
|
||||||
{% include "InvenTree/settings/label.html" %}
|
{% include "InvenTree/settings/label.html" %}
|
||||||
{% include "InvenTree/settings/report.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
|
// Javascript for Part Category panel
|
||||||
onPanelLoad('category', function() {
|
onPanelLoad('category', function() {
|
||||||
$('#category-select').select2({
|
$('#category-select').select2({
|
||||||
@ -136,11 +208,12 @@ onPanelLoad('category', function() {
|
|||||||
title: '{% trans "Default Value" %}',
|
title: '{% trans "Default Value" %}',
|
||||||
sortable: 'true',
|
sortable: 'true',
|
||||||
formatter: function(value, row, index, field) {
|
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>";
|
let buttons = '';
|
||||||
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>";
|
buttons += makeEditButton('template-edit', row.pk, '{% trans "Edit Template" %}');
|
||||||
|
buttons += makeDeleteButton('template-delete', row.pk, '{% trans "Delete Template" %}');
|
||||||
|
|
||||||
var html = value
|
let html = value
|
||||||
html += "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
html += wrapButtons(buttons);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -154,6 +227,7 @@ onPanelLoad('category', function() {
|
|||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
constructForm(`/api/part/category/parameters/${pk}/`, {
|
constructForm(`/api/part/category/parameters/${pk}/`, {
|
||||||
|
title: '{% trans "Edit Category Parameter Template" %}',
|
||||||
fields: {
|
fields: {
|
||||||
parameter_template: {},
|
parameter_template: {},
|
||||||
category: {
|
category: {
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
{% include "sidebar_item.html" with label='login' text=text icon="fa-fingerprint" %}
|
{% include "sidebar_item.html" with label='login' text=text icon="fa-fingerprint" %}
|
||||||
{% trans "Barcode Support" as text %}
|
{% trans "Barcode Support" as text %}
|
||||||
{% include "sidebar_item.html" with label='barcodes' text=text icon="fa-qrcode" %}
|
{% 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 %}
|
{% trans "Notifications" as text %}
|
||||||
{% include "sidebar_item.html" with label='global-notifications' text=text icon="fa-bell" %}
|
{% include "sidebar_item.html" with label='global-notifications' text=text icon="fa-bell" %}
|
||||||
{% trans "Pricing" as text %}
|
{% trans "Pricing" as text %}
|
||||||
|
@ -1123,9 +1123,9 @@ function loadSupplierPartTable(table, url, options) {
|
|||||||
var params = options.params || {};
|
var params = options.params || {};
|
||||||
|
|
||||||
// Load filters
|
// Load filters
|
||||||
var filters = loadTableFilters('supplier-part', params);
|
var filters = loadTableFilters('supplierpart', params);
|
||||||
|
|
||||||
setupFilterList('supplier-part', $(table));
|
setupFilterList('supplierpart', $(table));
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: url,
|
url: url,
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
renderOwner,
|
renderOwner,
|
||||||
renderPart,
|
renderPart,
|
||||||
renderPartCategory,
|
renderPartCategory,
|
||||||
|
renderProjectCode,
|
||||||
renderReturnOrder,
|
renderReturnOrder,
|
||||||
renderStockItem,
|
renderStockItem,
|
||||||
renderStockLocation,
|
renderStockLocation,
|
||||||
@ -78,6 +79,8 @@ function getModelRenderer(model) {
|
|||||||
return renderUser;
|
return renderUser;
|
||||||
case 'group':
|
case 'group':
|
||||||
return renderGroup;
|
return renderGroup;
|
||||||
|
case 'projectcode':
|
||||||
|
return renderProjectCode;
|
||||||
default:
|
default:
|
||||||
// Un-handled model type
|
// Un-handled model type
|
||||||
console.error(`Rendering not implemented for model '${model}'`);
|
console.error(`Rendering not implemented for model '${model}'`);
|
||||||
@ -476,3 +479,16 @@ function renderSupplierPart(data, parameters={}) {
|
|||||||
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: {},
|
supplier_reference: {},
|
||||||
|
project_code: {
|
||||||
|
icon: 'fa-list',
|
||||||
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
icon: 'fa-calendar-alt',
|
icon: 'fa-calendar-alt',
|
||||||
},
|
},
|
||||||
@ -126,6 +129,10 @@ function purchaseOrderFields(options={}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!global_settings.PROJECT_CODES_ENABLED) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1614,6 +1621,18 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
field: 'description',
|
field: 'description',
|
||||||
title: '{% trans "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',
|
field: 'status',
|
||||||
title: '{% trans "Status" %}',
|
title: '{% trans "Status" %}',
|
||||||
|
@ -46,6 +46,9 @@ function returnOrderFields(options={}) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
customer_reference: {},
|
customer_reference: {},
|
||||||
|
project_code: {
|
||||||
|
icon: 'fa-list',
|
||||||
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
icon: 'fa-calendar-alt',
|
icon: 'fa-calendar-alt',
|
||||||
},
|
},
|
||||||
@ -69,6 +72,10 @@ function returnOrderFields(options={}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!global_settings.PROJECT_CODES_ENABLED) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +278,18 @@ function loadReturnOrderTable(table, options={}) {
|
|||||||
field: 'description',
|
field: 'description',
|
||||||
title: '{% trans "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,
|
sortable: true,
|
||||||
field: 'status',
|
field: 'status',
|
||||||
|
@ -59,6 +59,9 @@ function salesOrderFields(options={}) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
customer_reference: {},
|
customer_reference: {},
|
||||||
|
project_code: {
|
||||||
|
icon: 'fa-list',
|
||||||
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
icon: 'fa-calendar-alt',
|
icon: 'fa-calendar-alt',
|
||||||
},
|
},
|
||||||
@ -82,6 +85,10 @@ function salesOrderFields(options={}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!global_settings.PROJECT_CODES_ENABLED) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -739,6 +746,18 @@ function loadSalesOrderTable(table, options) {
|
|||||||
field: 'description',
|
field: 'description',
|
||||||
title: '{% trans "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,
|
sortable: true,
|
||||||
field: 'status',
|
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_colortheme',
|
||||||
'common_inventreesetting',
|
'common_inventreesetting',
|
||||||
'common_inventreeusersetting',
|
'common_inventreeusersetting',
|
||||||
'common_webhookendpoint',
|
|
||||||
'common_webhookmessage',
|
|
||||||
'common_notificationentry',
|
'common_notificationentry',
|
||||||
'common_notificationmessage',
|
'common_notificationmessage',
|
||||||
'common_notesimage',
|
'common_notesimage',
|
||||||
|
'common_projectcode',
|
||||||
|
'common_webhookendpoint',
|
||||||
|
'common_webhookmessage',
|
||||||
'users_owner',
|
'users_owner',
|
||||||
|
|
||||||
# Third-party tables
|
# Third-party tables
|
||||||
|
Loading…
Reference in New Issue
Block a user