2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-13 06:48:44 +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

@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- [#11702](https://github.com/inventree/InvenTree/pull/11702) adds "last updated" and "updated by" fields for label and report templates, allowing users to track when a template was last modified and by whom.
- [#11685](https://github.com/inventree/InvenTree/pull/11685) exposes the data importer wizard to the plugin interface, allowing plugins to trigger the data importer wizard and perform custom data imports from the UI.
- [#11692](https://github.com/inventree/InvenTree/pull/11692) adds line item numbering for external orders (purchase, sales and return orders). This allows users to specify a line number for each line item on the order, which can be used for reference purposes. The line number is optional, and can be left blank if not required. The line number is stored as a string, to allow for more flexible formatting (e.g. "1", "1.1", "A", etc).
- [#11641](https://github.com/inventree/InvenTree/pull/11641) adds support for custom parameters against the SalesOrderShipment model.

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,6 +207,7 @@ class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
def save(self, *args, **kwargs):
"""Perform additional actions when the report is saved."""
if kwargs.pop('increment_revision', True):
# Increment revision number
self.revision += 1

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,