mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-13 14:58:47 +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:
@@ -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.
|
||||
|
||||
@@ -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