2
0
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:
Oliver
2026-04-09 16:10:23 +10:00
committed by GitHub
parent cdb8ad4c30
commit 9ce5f27375
7 changed files with 158 additions and 22 deletions

View File

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

View File

@@ -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."""

View File

@@ -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",
),
),
]

View File

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

View File

@@ -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."""

View File

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