From a92a1e134a630eeaf4baa26f8358e4ee613d6a64 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Fri, 18 Apr 2025 16:24:36 +0000 Subject: [PATCH] Report merge --- .pre-commit-config.yaml | 8 +- .../migrations/0030_reporttemplate_merge.py | 22 +++++ src/backend/InvenTree/report/models.py | 88 +++++++++++++++---- src/backend/InvenTree/report/serializers.py | 7 +- .../AdminCenter/ReportTemplatePanel.tsx | 6 ++ 5 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89659bcdc0..472cba0010 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: check-yaml - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.0 + rev: v0.11.6 hooks: - id: ruff-format args: [--preview] @@ -28,7 +28,7 @@ repos: --preview ] - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.6.6 + rev: 0.6.14 hooks: - id: pip-compile name: pip-compile requirements-dev.in @@ -70,13 +70,13 @@ repos: src/frontend/vite.config.ts | )$ - repo: https://github.com/biomejs/pre-commit - rev: v1.9.4 + rev: v2.0.0-beta.1 hooks: - id: biome-check additional_dependencies: ["@biomejs/biome@1.9.4"] files: ^src/frontend/.*\.(js|ts|tsx)$ - repo: https://github.com/gitleaks/gitleaks - rev: v8.24.0 + rev: v8.24.3 hooks: - id: gitleaks language_version: 1.23.6 diff --git a/src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py b/src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py new file mode 100644 index 0000000000..f08127369e --- /dev/null +++ b/src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.20 on 2025-04-03 01:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("report", "0029_remove_reportoutput_template_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="reporttemplate", + name="merge", + field=models.BooleanField( + default=False, + help_text="Render a single report against selected items", + verbose_name="Merge", + ), + ), + ] diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 4383f5b833..b8ed4784d4 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -170,10 +170,12 @@ class ReportContextExtension(TypedDict): Attributes: page_size: The page size of the report landscape: Boolean value, True if the report is in landscape mode + merge: Boolean value, True if the a single report is generated against multiple items """ page_size: str landscape: bool + merge: bool class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel): @@ -366,6 +368,12 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): help_text=_('Render report in landscape orientation'), ) + merge = models.BooleanField( + default=False, + verbose_name=_('Merge'), + help_text=_('Render a single report against selected items'), + ) + def get_report_size(self) -> str: """Return the printable page size for this report.""" try: @@ -382,14 +390,21 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): return page_size - def get_context(self, instance, request=None, **kwargs): - """Supply context data to the report template for rendering.""" - base_context = super().get_context(instance, request) + def get_report_context(self): + """Return report template context.""" report_context: ReportContextExtension = { 'page_size': self.get_report_size(), 'landscape': self.landscape, + 'merge': self.merge, } + return report_context + + def get_context(self, instance, request=None, **kwargs): + """Supply context data to the report template for rendering.""" + base_context = super().get_context(instance, request) + report_context: ReportContextExtension = self.get_report_context() + context = {**base_context, **report_context} # Pass the context through to the plugin registry for any additional information @@ -455,21 +470,23 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): output.save() try: - for instance in items: - context = self.get_context(instance, request) + if self.merge: + base_context = super().base_context(request) + report_context = self.get_report_context() + item_contexts = [] + for instance in items: + item_contexts.append(instance.report_context()) + contexts = { + **base_context, + **report_context, + 'instances': item_contexts, + } if report_name is None: - report_name = self.generate_filename(context) + report_name = self.generate_filename(contexts) - # Render the report output - try: - if debug_mode: - report = self.render_as_string(instance, request) - else: - report = self.render(instance, request) - except TemplateDoesNotExist as e: - t_name = str(e) or self.template - raise ValidationError(f'Template file {t_name} does not exist') + html = render_to_string(self.template_name, contexts, request) + report = HTML(string=html).write_pdf() outputs.append(report) @@ -495,6 +512,47 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase): # Update the progress of the report generation output.progress += 1 output.save() + else: + for instance in items: + context = self.get_context(instance, request) + + if report_name is None: + report_name = self.generate_filename(context) + + # Render the report output + try: + if debug_mode: + report = self.render_as_string(instance, request) + else: + report = self.render(instance, request) + except TemplateDoesNotExist as e: + t_name = str(e) or self.template + raise ValidationError(f'Template file {t_name} does not exist') + + outputs.append(report) + + # Attach the generated report to the model instance (if required) + if self.attach_to_model and not debug_mode: + instance.create_attachment( + attachment=ContentFile(report, report_name), + comment=_(f'Report generated from template {self.name}'), + upload_user=request.user + if request and request.user.is_authenticated + else None, + ) + + # Provide generated report to any interested plugins + for plugin in report_plugins: + try: + plugin.report_callback(self, instance, report, request) + except Exception: + InvenTree.exceptions.log_error( + f'plugins.{plugin.slug}.report_callback' + ) + + # Update the progress of the report generation + output.progress += 1 + output.save() except Exception as exc: # Something went wrong during the report generation process diff --git a/src/backend/InvenTree/report/serializers.py b/src/backend/InvenTree/report/serializers.py index fe542742ff..0f133898f6 100644 --- a/src/backend/InvenTree/report/serializers.py +++ b/src/backend/InvenTree/report/serializers.py @@ -65,7 +65,12 @@ class ReportTemplateSerializer(ReportSerializerBase): """Metaclass options.""" model = report.models.ReportTemplate - fields = [*ReportSerializerBase.base_fields(), 'page_size', 'landscape'] + fields = [ + *ReportSerializerBase.base_fields(), + 'page_size', + 'landscape', + 'merge', + ] page_size = serializers.ChoiceField( required=False, diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/ReportTemplatePanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/ReportTemplatePanel.tsx index 6e107fc954..a098760d4c 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/ReportTemplatePanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/ReportTemplatePanel.tsx @@ -21,6 +21,12 @@ function ReportTemplateTable() { ) }, + merge: { + label: t`Merge`, + modelRenderer: (instance: any) => ( + + ) + }, attach_to_model: { label: t`Attach to Model`, modelRenderer: (instance: any) => (