mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-15 07:48:51 +00:00
Template Updates (#11702)
* Display filename pattern in template tables * Add user update tracking to template models * Update API / serializers * Capture user information via API * Display update information in tables * Bump API version and CHANGELOG.md * Prevent double increment of revision * Fix
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 474
|
||||
INVENTREE_API_VERSION = 475
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v475 -> 2026-04-09 : https://github.com/inventree/InvenTree/pull/11702
|
||||
- Adds "updated" and "updated_by" fields to the LabelTemplate and ReportTemplate API endpoints
|
||||
|
||||
v474 -> 2026-04-08 : https://github.com/inventree/InvenTree/pull/11693
|
||||
- Adds DataImportMixin to the ManufacturerPartList API endpoint
|
||||
|
||||
|
||||
@@ -224,23 +224,27 @@ class LabelPrint(GenericAPIView):
|
||||
return Response(DataOutputSerializer(output).data, status=201)
|
||||
|
||||
|
||||
class LabelTemplateList(TemplatePermissionMixin, ListCreateAPI):
|
||||
class LabelTemplateMixin:
|
||||
"""Mixin class for label template API views."""
|
||||
|
||||
queryset = report.models.LabelTemplate.objects.all().prefetch_related('updated_by')
|
||||
serializer_class = report.serializers.LabelTemplateSerializer
|
||||
|
||||
|
||||
class LabelTemplateList(TemplatePermissionMixin, LabelTemplateMixin, ListCreateAPI):
|
||||
"""API endpoint for viewing list of LabelTemplate objects."""
|
||||
|
||||
queryset = report.models.LabelTemplate.objects.all()
|
||||
serializer_class = report.serializers.LabelTemplateSerializer
|
||||
filterset_class = LabelFilter
|
||||
filter_backends = [DjangoFilterBackend, InvenTreeSearchFilter]
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'enabled']
|
||||
|
||||
|
||||
class LabelTemplateDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
|
||||
class LabelTemplateDetail(
|
||||
TemplatePermissionMixin, LabelTemplateMixin, RetrieveUpdateDestroyAPI
|
||||
):
|
||||
"""Detail API endpoint for label template model."""
|
||||
|
||||
queryset = report.models.LabelTemplate.objects.all()
|
||||
serializer_class = report.serializers.LabelTemplateSerializer
|
||||
|
||||
|
||||
class ReportPrint(GenericAPIView):
|
||||
"""API endpoint for printing reports."""
|
||||
@@ -300,23 +304,27 @@ class ReportPrint(GenericAPIView):
|
||||
return Response(DataOutputSerializer(output).data, status=201)
|
||||
|
||||
|
||||
class ReportTemplateList(TemplatePermissionMixin, ListCreateAPI):
|
||||
class ReportTemplateMixin:
|
||||
"""Mixin class for report template API views."""
|
||||
|
||||
queryset = report.models.ReportTemplate.objects.all().prefetch_related('updated_by')
|
||||
serializer_class = report.serializers.ReportTemplateSerializer
|
||||
|
||||
|
||||
class ReportTemplateList(TemplatePermissionMixin, ReportTemplateMixin, ListCreateAPI):
|
||||
"""API endpoint for viewing list of ReportTemplate objects."""
|
||||
|
||||
queryset = report.models.ReportTemplate.objects.all()
|
||||
serializer_class = report.serializers.ReportTemplateSerializer
|
||||
filterset_class = ReportFilter
|
||||
filter_backends = [DjangoFilterBackend, InvenTreeSearchFilter]
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['name', 'enabled']
|
||||
|
||||
|
||||
class ReportTemplateDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
|
||||
class ReportTemplateDetail(
|
||||
TemplatePermissionMixin, ReportTemplateMixin, RetrieveUpdateDestroyAPI
|
||||
):
|
||||
"""Detail API endpoint for report template model."""
|
||||
|
||||
queryset = report.models.ReportTemplate.objects.all()
|
||||
serializer_class = report.serializers.ReportTemplateSerializer
|
||||
|
||||
|
||||
class ReportSnippetList(TemplatePermissionMixin, ListCreateAPI):
|
||||
"""API endpoint for listing ReportSnippet objects."""
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-09 01:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("report", "0031_reporttemplate_merge"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="labeltemplate",
|
||||
name="updated",
|
||||
field=models.DateTimeField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Timestamp of last update",
|
||||
null=True,
|
||||
verbose_name="Updated",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="labeltemplate",
|
||||
name="updated_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who last updated this object",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Update By",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reporttemplate",
|
||||
name="updated",
|
||||
field=models.DateTimeField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Timestamp of last update",
|
||||
null=True,
|
||||
verbose_name="Updated",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reporttemplate",
|
||||
name="updated_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who last updated this object",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Update By",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -27,7 +27,7 @@ import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
import report.helpers
|
||||
import report.validators
|
||||
from common.models import DataOutput, RenderChoices
|
||||
from common.models import DataOutput, RenderChoices, UpdatedUserMixin
|
||||
from common.settings import get_global_setting
|
||||
from InvenTree.helpers_model import get_base_url
|
||||
from InvenTree.models import MetadataMixin
|
||||
@@ -189,7 +189,9 @@ class ReportContextExtension(TypedDict):
|
||||
merge: bool
|
||||
|
||||
|
||||
class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
|
||||
class ReportTemplateBase(
|
||||
MetadataMixin, UpdatedUserMixin, InvenTree.models.InvenTreeModel
|
||||
):
|
||||
"""Base class for reports, labels."""
|
||||
|
||||
class ModelChoices(RenderChoices):
|
||||
@@ -205,8 +207,9 @@ class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Perform additional actions when the report is saved."""
|
||||
# Increment revision number
|
||||
self.revision += 1
|
||||
if kwargs.pop('increment_revision', True):
|
||||
# Increment revision number
|
||||
self.revision += 1
|
||||
|
||||
super().save()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from InvenTree.serializers import (
|
||||
InvenTreeAttachmentSerializerField,
|
||||
InvenTreeModelSerializer,
|
||||
)
|
||||
from users.serializers import UserSerializer
|
||||
|
||||
|
||||
class ReportSerializerBase(InvenTreeModelSerializer):
|
||||
@@ -27,6 +28,21 @@ class ReportSerializerBase(InvenTreeModelSerializer):
|
||||
if len(self.fields['model_type'].choices) == 0:
|
||||
self.fields['model_type'].choices = report.helpers.report_model_options()
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""Override the save method to capture the user information."""
|
||||
user = self.context.get('request').user
|
||||
|
||||
if not user or not user.is_authenticated:
|
||||
raise PermissionError(
|
||||
_('User must be authenticated to save report templates')
|
||||
)
|
||||
|
||||
instance = super().save(**kwargs)
|
||||
instance.updated_by = user
|
||||
instance.save(increment_revision=False)
|
||||
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def base_fields():
|
||||
"""Base serializer field set."""
|
||||
@@ -41,6 +57,9 @@ class ReportSerializerBase(InvenTreeModelSerializer):
|
||||
'enabled',
|
||||
'revision',
|
||||
'attach_to_model',
|
||||
'updated',
|
||||
'updated_by',
|
||||
'updated_by_detail',
|
||||
]
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
@@ -56,6 +75,10 @@ class ReportSerializerBase(InvenTreeModelSerializer):
|
||||
allow_null=False,
|
||||
)
|
||||
|
||||
updated_by_detail = UserSerializer(
|
||||
source='updated_by', read_only=True, allow_null=True, many=False
|
||||
)
|
||||
|
||||
|
||||
class ReportTemplateSerializer(ReportSerializerBase):
|
||||
"""Serializer class for report template model."""
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
TemplateEditorUIFeature,
|
||||
TemplatePreviewUIFeature
|
||||
} from '../../components/plugins/PluginUIFeatureTypes';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import { useFilters } from '../../hooks/UseFilter';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
@@ -48,8 +49,13 @@ import { useInstance } from '../../hooks/UseInstance';
|
||||
import { usePluginUIFeature } from '../../hooks/UsePluginUIFeature';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
|
||||
import {
|
||||
BooleanColumn,
|
||||
DescriptionColumn,
|
||||
UserColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
export type TemplateI = {
|
||||
pk: number;
|
||||
@@ -233,12 +239,40 @@ export function TemplateTable({
|
||||
{
|
||||
accessor: 'revision',
|
||||
sortable: false,
|
||||
switchable: true
|
||||
switchable: true,
|
||||
render: (record: any) => {
|
||||
return (
|
||||
<Group gap='xs' justify='space-between'>
|
||||
<Text size='sm'>{record.revision}</Text>
|
||||
{record.updated && (
|
||||
<TableHoverCard
|
||||
value=''
|
||||
title={t`Last Updated`}
|
||||
extra={<Text size='xs'>{formatDate(record.updated)}</Text>}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
},
|
||||
UserColumn({
|
||||
accessor: 'updated_by_detail',
|
||||
sortable: false,
|
||||
defaultVisible: false,
|
||||
title: t`Updated By`
|
||||
}),
|
||||
{
|
||||
accessor: 'filters',
|
||||
sortable: false,
|
||||
switchable: true
|
||||
switchable: true,
|
||||
defaultVisible: false
|
||||
},
|
||||
{
|
||||
accessor: 'filename_pattern',
|
||||
title: t`Filename`,
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
defaultVisible: false
|
||||
},
|
||||
...Object.entries(additionalFormFields || {}).map(([key, field]) => ({
|
||||
accessor: key,
|
||||
|
||||
Reference in New Issue
Block a user