From a92a1e134a630eeaf4baa26f8358e4ee613d6a64 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Fri, 18 Apr 2025 16:24:36 +0000 Subject: [PATCH 01/20] Report merge --- .pre-commit-config.yaml | 8 +- .../migrations/0030_reporttemplate_merge.py | 22 +++++ src/backend/InvenTree/report/models.py | 90 +++++++++++++++---- src/backend/InvenTree/report/serializers.py | 7 +- .../AdminCenter/ReportTemplatePanel.tsx | 6 ++ 5 files changed, 112 insertions(+), 21 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 89659bcdc00c..472cba0010df 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 000000000000..f08127369e71 --- /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 4383f5b8336c..b8ed4784d426 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 @@ def __init__(self, *args, **kwargs): 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 @@ def get_report_size(self) -> str: 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 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: 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) - - # 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') + report_name = self.generate_filename(contexts) + + html = render_to_string(self.template_name, contexts, request) + report = HTML(string=html).write_pdf() outputs.append(report) @@ -495,6 +512,47 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: # 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 fe542742ff0d..0f133898f6de 100644 --- a/src/backend/InvenTree/report/serializers.py +++ b/src/backend/InvenTree/report/serializers.py @@ -65,7 +65,12 @@ class Meta: """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 6e107fc954f4..a098760d4c83 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) => ( From 25eef24ce22e2d8ab34e5d9ae2af71d1563f7fc8 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Fri, 18 Apr 2025 16:35:53 +0000 Subject: [PATCH 02/20] Remove auto-generated file --- .../migrations/0030_reporttemplate_merge.py | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py diff --git a/src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py b/src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py deleted file mode 100644 index f08127369e71..000000000000 --- a/src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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", - ), - ), - ] From 00d80bae2a551d103473bb932efa03cff18b5e9f Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Fri, 18 Apr 2025 17:00:09 +0000 Subject: [PATCH 03/20] Remove pre-commit file --- .pre-commit-config.yaml | 90 ----------------------------------------- 1 file changed, 90 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 472cba0010df..000000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -exclude: | - (?x)^( - src/backend/InvenTree/InvenTree/static/.*| - src/backend/InvenTree/locale/.*| - src/frontend/src/locales/.* | - .*/migrations/.* | - src/frontend/yarn.lock - )$ -repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: mixed-line-ending -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 - hooks: - - id: ruff-format - args: [--preview] - - id: ruff - args: [ - --fix, - # --unsafe-fixes, - --preview - ] -- repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.6.14 - hooks: - - id: pip-compile - name: pip-compile requirements-dev.in - args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt, --no-strip-extras, --generate-hashes] - files: src/backend/requirements-dev\.(in|txt)$ - - id: pip-compile - name: pip-compile requirements.txt - args: [src/backend/requirements.in, -o, src/backend/requirements.txt, --no-strip-extras, --generate-hashes] - files: src/backend/requirements\.(in|txt)$ - - id: pip-compile - name: pip-compile requirements.txt - args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt, --no-strip-extras, --generate-hashes] - files: contrib/dev_reqs/requirements\.(in|txt)$ - - id: pip-compile - name: pip-compile requirements.txt - args: [docs/requirements.in, -o, docs/requirements.txt, --no-strip-extras, --generate-hashes] - files: docs/requirements\.(in|txt)$ - - id: pip-compile - name: pip-compile requirements.txt - args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, --no-strip-extras, --generate-hashes] - files: contrib/container/requirements\.(in|txt)$ -- repo: https://github.com/Riverside-Healthcare/djLint - rev: v1.36.4 - hooks: - - id: djlint-django -- repo: https://github.com/codespell-project/codespell - rev: v2.4.1 - hooks: - - id: codespell - additional_dependencies: - - tomli - exclude: > - (?x)^( - docs/docs/stylesheets/.*| - docs/docs/javascripts/.*| - docs/docs/webfonts/.* | - src/frontend/src/locales/.* | - pyproject.toml | - src/frontend/vite.config.ts | - )$ -- repo: https://github.com/biomejs/pre-commit - 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.3 - hooks: - - id: gitleaks - language_version: 1.23.6 -#- repo: https://github.com/jumanjihouse/pre-commit-hooks -# rev: 3.0.0 -# hooks: -# - id: shellcheck -- repo: https://github.com/isidentical/teyit - rev: 0.4.3 - hooks: - - id: teyit From 1c43864db19fa8217538ad1911f5828facd062bb Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Fri, 18 Apr 2025 17:02:21 +0000 Subject: [PATCH 04/20] Revert "Remove pre-commit file" This reverts commit 00d80bae2a551d103473bb932efa03cff18b5e9f. --- .pre-commit-config.yaml | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000000..472cba0010df --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,90 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +exclude: | + (?x)^( + src/backend/InvenTree/InvenTree/static/.*| + src/backend/InvenTree/locale/.*| + src/frontend/src/locales/.* | + .*/migrations/.* | + src/frontend/yarn.lock + )$ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: mixed-line-ending +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.6 + hooks: + - id: ruff-format + args: [--preview] + - id: ruff + args: [ + --fix, + # --unsafe-fixes, + --preview + ] +- repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.6.14 + hooks: + - id: pip-compile + name: pip-compile requirements-dev.in + args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt, --no-strip-extras, --generate-hashes] + files: src/backend/requirements-dev\.(in|txt)$ + - id: pip-compile + name: pip-compile requirements.txt + args: [src/backend/requirements.in, -o, src/backend/requirements.txt, --no-strip-extras, --generate-hashes] + files: src/backend/requirements\.(in|txt)$ + - id: pip-compile + name: pip-compile requirements.txt + args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt, --no-strip-extras, --generate-hashes] + files: contrib/dev_reqs/requirements\.(in|txt)$ + - id: pip-compile + name: pip-compile requirements.txt + args: [docs/requirements.in, -o, docs/requirements.txt, --no-strip-extras, --generate-hashes] + files: docs/requirements\.(in|txt)$ + - id: pip-compile + name: pip-compile requirements.txt + args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, --no-strip-extras, --generate-hashes] + files: contrib/container/requirements\.(in|txt)$ +- repo: https://github.com/Riverside-Healthcare/djLint + rev: v1.36.4 + hooks: + - id: djlint-django +- repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + additional_dependencies: + - tomli + exclude: > + (?x)^( + docs/docs/stylesheets/.*| + docs/docs/javascripts/.*| + docs/docs/webfonts/.* | + src/frontend/src/locales/.* | + pyproject.toml | + src/frontend/vite.config.ts | + )$ +- repo: https://github.com/biomejs/pre-commit + 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.3 + hooks: + - id: gitleaks + language_version: 1.23.6 +#- repo: https://github.com/jumanjihouse/pre-commit-hooks +# rev: 3.0.0 +# hooks: +# - id: shellcheck +- repo: https://github.com/isidentical/teyit + rev: 0.4.3 + hooks: + - id: teyit From b4ce88b56f9e428f72617b67f1d7e1bc0e488243 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Fri, 18 Apr 2025 17:13:58 +0000 Subject: [PATCH 05/20] Update API version --- src/backend/InvenTree/InvenTree/api_version.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index a8d72c24745e..a207a30b3181 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 338 +INVENTREE_API_VERSION = 339 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v339 -> 2025-04-18 : https://github.com/inventree/InvenTree/pull/9532 + - Adds "merge" field to the ReportTemplate model + v338 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9333 - Adds oAuth2 support for the API From 5f58cf5925f80ae947fd9a620b33f52f85deba09 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Mon, 21 Apr 2025 02:52:05 +0000 Subject: [PATCH 06/20] Reduced duplicated logic --- src/backend/InvenTree/report/models.py | 93 +++++++++++++------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 5b2b1af37b3d..36eef97a5e5f 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -233,31 +233,34 @@ def generate_filename(self, context, **kwargs): return template_string.render(Context(context)) - def render_as_string(self, instance, request=None, **kwargs) -> str: + def render_as_string(self, instance, request=None, context=None, **kwargs) -> str: """Render the report to a HTML string. Arguments: instance: The model instance to render against request: A HTTPRequest object (optional) + context: DTL context (optional) Returns: str: HTML string """ - context = self.get_context(instance, request, **kwargs) + if context is None: + context = self.get_context(instance, request, **kwargs) return render_to_string(self.template_name, context, request) - def render(self, instance, request=None, **kwargs) -> bytes: + def render(self, instance, request=None, context=None, **kwargs) -> bytes: """Render the template to a PDF file. Arguments: instance: The model instance to render against request: A HTTPRequest object (optional) + context: DTL context (optional) Returns: bytes: PDF data """ - html = self.render_as_string(instance, request, **kwargs) + html = self.render_as_string(instance, request, context, **kwargs) pdf = HTML(string=html).write_pdf() return pdf @@ -424,6 +427,27 @@ def get_context(self, instance, request=None, **kwargs): return context + def handle_attachment(self, instance, report, report_name, request, debug_mode): + """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, + ) + + def notify_plugins(self, instance, report, request): + """Provide generated report to any interested plugins.""" + report_plugins = registry.with_mixin(PluginMixinEnum.REPORT) + + 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') + def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: """Print reports for a list of items against this template. @@ -455,7 +479,7 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: # Start with a default report name report_name = None - report_plugins = registry.with_mixin(PluginMixinEnum.REPORT) + # report_plugins = registry.with_mixin(PluginMixinEnum.REPORT) # If a DataOutput object is not provided, create a new one if not output: @@ -482,6 +506,7 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: item_contexts = [] for instance in items: item_contexts.append(instance.report_context()) + contexts = { **base_context, **report_context, @@ -491,29 +516,21 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: if report_name is None: report_name = self.generate_filename(contexts) - html = render_to_string(self.template_name, contexts, request) - report = HTML(string=html).write_pdf() + try: + if debug_mode: + report = self.render_as_string(instance, request, contexts) + else: + report = self.render(instance, request, contexts) + 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' - ) + self.handle_attachment( + instance, report, report_name, request, debug_mode + ) + self.notify_plugins(instance, report, request) # Update the progress of the report generation output.progress += 1 @@ -528,33 +545,19 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: # Render the report output try: if debug_mode: - report = self.render_as_string(instance, request) + report = self.render_as_string(instance, request, None) else: - report = self.render(instance, request) + report = self.render(instance, request, None) 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' - ) + self.handle_attachment( + instance, report, report_name, request, debug_mode + ) + self.notify_plugins(instance, report, request) # Update the progress of the report generation output.progress += 1 From 5fb3638acf4bc13377928fc6f8957787a247e121 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 21 Apr 2025 11:43:31 +0200 Subject: [PATCH 07/20] reset pre-commit config --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 472cba0010df..89659bcdc00c 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.6 + rev: v0.11.0 hooks: - id: ruff-format args: [--preview] @@ -28,7 +28,7 @@ repos: --preview ] - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.6.14 + rev: 0.6.6 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: v2.0.0-beta.1 + rev: v1.9.4 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.3 + rev: v8.24.0 hooks: - id: gitleaks language_version: 1.23.6 From 497706e9145668bc0c385fc2570d52cc864c3649 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Wed, 23 Apr 2025 01:14:39 +0000 Subject: [PATCH 08/20] Added migration files --- .../migrations/0030_reporttemplate_merge.py | 22 +++++++++++++++++++ .../migrations/0031_merge_20250420_2336.py | 13 +++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py create mode 100644 src/backend/InvenTree/report/migrations/0031_merge_20250420_2336.py 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 000000000000..f08127369e71 --- /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/migrations/0031_merge_20250420_2336.py b/src/backend/InvenTree/report/migrations/0031_merge_20250420_2336.py new file mode 100644 index 000000000000..b63223596647 --- /dev/null +++ b/src/backend/InvenTree/report/migrations/0031_merge_20250420_2336.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.20 on 2025-04-20 23:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("report", "0030_alter_labeltemplate_model_type_and_more"), + ("report", "0030_reporttemplate_merge"), + ] + + operations = [] From 7ef03b0b703014b4b23a994b3d2ee3e79f3a18f5 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Thu, 24 Apr 2025 01:15:00 +0000 Subject: [PATCH 09/20] Added unit test --- src/backend/InvenTree/report/tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index 05333266baec..a4fbaa001a04 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -486,6 +486,16 @@ def test_print(self): # The attachment should be a PDF self.assertTrue(attachment.attachment.name.endswith('.pdf')) + # Set merge = True, test report is still generated + template.merge = True + response = self.post( + url, {'template': template.pk, 'items': [item.pk]}, expected_code=201 + ) + + # There should be a link to the generated PDF + self.assertTrue(response.data['output'].startswith('/media/data_output/')) + self.assertTrue(response.data['output'].endswith('.pdf')) + def test_mdl_build(self): """Test the Build model.""" self.run_print_test(Build, 'build', label=False) From 02a9af85a1e4679852aefa71a15b1d8f8e776d46 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Thu, 24 Apr 2025 02:03:39 +0000 Subject: [PATCH 10/20] Removed redundant migration --- .../report/migrations/0031_merge_20250420_2336.py | 13 ------------- ...mplate_merge.py => 0031_reporttemplate_merge.py} | 0 2 files changed, 13 deletions(-) delete mode 100644 src/backend/InvenTree/report/migrations/0031_merge_20250420_2336.py rename src/backend/InvenTree/report/migrations/{0030_reporttemplate_merge.py => 0031_reporttemplate_merge.py} (100%) diff --git a/src/backend/InvenTree/report/migrations/0031_merge_20250420_2336.py b/src/backend/InvenTree/report/migrations/0031_merge_20250420_2336.py deleted file mode 100644 index b63223596647..000000000000 --- a/src/backend/InvenTree/report/migrations/0031_merge_20250420_2336.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 4.2.20 on 2025-04-20 23:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("report", "0030_alter_labeltemplate_model_type_and_more"), - ("report", "0030_reporttemplate_merge"), - ] - - operations = [] diff --git a/src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py b/src/backend/InvenTree/report/migrations/0031_reporttemplate_merge.py similarity index 100% rename from src/backend/InvenTree/report/migrations/0030_reporttemplate_merge.py rename to src/backend/InvenTree/report/migrations/0031_reporttemplate_merge.py From 4299338fc12418545d3caa8ec9b68d46b21fb50e Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Wed, 30 Apr 2025 02:55:39 +0000 Subject: [PATCH 11/20] Updated migration file --- .../InvenTree/report/migrations/0031_reporttemplate_merge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/InvenTree/report/migrations/0031_reporttemplate_merge.py b/src/backend/InvenTree/report/migrations/0031_reporttemplate_merge.py index f08127369e71..184abe45c8d7 100644 --- a/src/backend/InvenTree/report/migrations/0031_reporttemplate_merge.py +++ b/src/backend/InvenTree/report/migrations/0031_reporttemplate_merge.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("report", "0029_remove_reportoutput_template_and_more"), + ("report", "0030_alter_labeltemplate_model_type_and_more"), ] operations = [ From aeda5548b00c25077cfd44b4be775b4a7488fd31 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Wed, 30 Apr 2025 02:58:32 +0000 Subject: [PATCH 12/20] Added a default report template with merge enabled --- src/backend/InvenTree/report/apps.py | 7 + .../report/inventree_stock_report_merge.html | 127 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/backend/InvenTree/report/templates/report/inventree_stock_report_merge.html diff --git a/src/backend/InvenTree/report/apps.py b/src/backend/InvenTree/report/apps.py index cf7d679ffd92..eafa43c46a13 100644 --- a/src/backend/InvenTree/report/apps.py +++ b/src/backend/InvenTree/report/apps.py @@ -224,6 +224,13 @@ def create_default_reports(self): 'description': 'Sample stock item test report', 'model_type': 'stockitem', }, + { + 'file': 'inventree_stock_report_merge.html', + 'name': 'InvenTree Default Stock Report Merge', + 'description': 'Sample stock item report merge', + 'model_type': 'stockitem', + 'merge': True, + }, { 'file': 'inventree_stock_location_report.html', 'name': 'InvenTree Stock Location Report', diff --git a/src/backend/InvenTree/report/templates/report/inventree_stock_report_merge.html b/src/backend/InvenTree/report/templates/report/inventree_stock_report_merge.html new file mode 100644 index 000000000000..3f97342ebee6 --- /dev/null +++ b/src/backend/InvenTree/report/templates/report/inventree_stock_report_merge.html @@ -0,0 +1,127 @@ +{% extends "report/inventree_report_base.html" %} + +{% load i18n %} +{% load report %} +{% load inventree_extras %} + +{% block style %} +.test-table { + width: 100%; +} + +{% block bottom_left %} +content: "{% format_date date %}"; +{% endblock bottom_left %} + +{% block bottom_center %} +content: "{% inventree_version shortstring=True %}"; +{% endblock bottom_center %} + +{% block top_center %} +content: "{% trans 'Stock Item Test Report' %}"; +{% endblock top_center %} + +.test-row { + padding: 3px; +} + +.test-pass { + color: #5f5; +} + +.test-fail { + color: #F55; +} + +.test-not-found { + color: #33A; +} + +.required-test-not-found { + color: #EEE; + background-color: #F55; +} + +.container { + padding: 5px; + border: 1px solid; +} + +.text-left { + display: inline-block; + width: 50%; +} + +.img-right { + display: inline; + align-content: right; + align-items: right; + width: 50%; +} + +.part-img { + height: 4cm; +} + +{% endblock style %} + +{% block pre_page_content %} + +{% endblock pre_page_content %} + +{% block page_content %} +{% for item in instances %} +
+
+

+ {{ item.part.full_name }} +

+

{{ item.part.description }}

+

{{ item.stock_item.location }}

+

Stock Item ID: {{ item.stock_item.pk }}

+
+
+ {% trans "Part image" %} +
+

+ {% if item.stock_item.is_serialized %} + {% trans "Serial Number" %}: {{ item.stock_item.serial }} + {% else %} + {% trans "Quantity" %}: {% decimal item.stock_item.quantity %} + {% endif %} +

+
+
+ +{% if item.installed_items|length > 0 %} +

{% trans "Installed Items" %}

+ + + + + + {% for sub_item in item.installed_items %} + + + + + {% endfor %} + +
+ {% trans "Part image" %} + {{ sub_item.part.full_name }} + + {% if sub_item.serialized %} + {% trans "Serial" %}: {{ sub_item.serial }} + {% else %} + {% trans "Quantity" %}: {% decimal sub_item.quantity %} + {% endif %} +
+ +{% endif %} +{% endfor %} +{% endblock page_content %} + +{% block post_page_content %} + +{% endblock post_page_content %} From e4eb54498a8ca79e1d7d4b55ed9523aa2fb5d526 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Wed, 30 Apr 2025 03:01:37 +0000 Subject: [PATCH 13/20] Unit test to ensure a single page is generated --- src/backend/InvenTree/report/tests.py | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index a4fbaa001a04..5c036c4bce28 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -9,6 +9,7 @@ import report.models as report_models from build.models import Build from common.models import Attachment +from common.settings import set_global_setting from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase from order.models import ReturnOrder, SalesOrder from part.models import Part @@ -486,15 +487,32 @@ def test_print(self): # The attachment should be a PDF self.assertTrue(attachment.attachment.name.endswith('.pdf')) - # Set merge = True, test report is still generated - template.merge = True + # Set DEBUG_MODE to return the report as an HTML file + set_global_setting('REPORT_DEBUG_MODE', True) + + # Grab the report template + template_merge = ReportTemplate.objects.filter( + enabled=True, model_type='stockitem', merge=True + ).first() + + # Grab the first 3 stock items + items = StockItem.objects.all()[:3] response = self.post( - url, {'template': template.pk, 'items': [item.pk]}, expected_code=201 + url, + {'template': template_merge.pk, 'items': [item.pk for item in items]}, + expected_code=201, ) - # There should be a link to the generated PDF - self.assertTrue(response.data['output'].startswith('/media/data_output/')) - self.assertTrue(response.data['output'].endswith('.pdf')) + # Open and read the output HTML as a string + html_report = '' + path_prefix = '/home/inventree/dev' + with open(path_prefix + response.data['output'], encoding='utf-8') as f: + html_report = f.read() + + # Assuming the number of and correlates to the number of pages + # in the generated PDF + self.assertEqual(html_report.count(''), 1) + self.assertEqual(html_report.count(''), 1) def test_mdl_build(self): """Test the Build model.""" From effd432456912f37a320e182ec4071e61882f778 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Thu, 1 May 2025 02:10:16 +0000 Subject: [PATCH 14/20] Added docs to support merge feature --- docs/docs/report/context_variables.md | 2 ++ docs/docs/report/samples.md | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/docs/docs/report/context_variables.md b/docs/docs/report/context_variables.md index 04428be83f3e..ed5d0a10e89e 100644 --- a/docs/docs/report/context_variables.md +++ b/docs/docs/report/context_variables.md @@ -23,6 +23,8 @@ In addition to the [global context](#global-context), all *report* templates hav {{ report_context("base", "report") }} +`merge` exposes the selected items in the `items` context variable. {{ templatefile("report/inventree_stock_report_merge.html") }} shows an example on how to use this context variable. + Note that custom plugins may also add additional context variables to the report context. ::: report.models.ReportTemplate.get_context diff --git a/docs/docs/report/samples.md b/docs/docs/report/samples.md index 493728bf1b89..a3b9b8561995 100644 --- a/docs/docs/report/samples.md +++ b/docs/docs/report/samples.md @@ -22,6 +22,7 @@ The following report templates are provided "out of the box" and can be used as | [Sales Order Shipment](#sales-order-shipment) | [SalesOrderShipment](../order/sales_order.md) | Sales Order Shipment report | | [Stock Location](#stock-location) | [StockLocation](../stock/stock.md#stock-location) | Stock Location report | | [Test Report](#test-report) | [StockItem](../stock/stock.md#stock-item) | Test Report | +| [Selected Stock Items Report](#selected-stock-items-report) | [StockItem](../stock/stock.md#stock-item) | Selected Stock Items report | ### Bill of Materials Report @@ -55,6 +56,10 @@ The following report templates are provided "out of the box" and can be used as {{ templatefile("report/inventree_test_report.html") }} +### Selected Stock Items Report + +{{ templatefile("report/inventree_stock_report_merge.html") }} + ## Label Templates The following label templates are provided "out of the box" and can be used as a starting point, or as a reference for creating custom label templates: From ae979e13471105edc828fe49ba022b17cd703da5 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Thu, 1 May 2025 20:36:25 +0000 Subject: [PATCH 15/20] Clean up --- src/backend/InvenTree/report/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 36eef97a5e5f..67deefb32a3f 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -239,7 +239,7 @@ def render_as_string(self, instance, request=None, context=None, **kwargs) -> st Arguments: instance: The model instance to render against request: A HTTPRequest object (optional) - context: DTL context (optional) + context: Django template language contexts (optional) Returns: str: HTML string @@ -479,8 +479,6 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: # Start with a default report name report_name = None - # report_plugins = registry.with_mixin(PluginMixinEnum.REPORT) - # If a DataOutput object is not provided, create a new one if not output: output = DataOutput.objects.create( From 33bbc8bb22ae3970dc2ce0c8cf712c1e3d1a8533 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Thu, 1 May 2025 21:19:17 +0000 Subject: [PATCH 16/20] Clean up --- src/backend/InvenTree/report/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 67deefb32a3f..2aad6dec759e 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -255,7 +255,7 @@ def render(self, instance, request=None, context=None, **kwargs) -> bytes: Arguments: instance: The model instance to render against request: A HTTPRequest object (optional) - context: DTL context (optional) + context: Django template langaguage contexts (optional) Returns: bytes: PDF data From dcb10405bc8515c036328418e8197594cd67f145 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Mon, 5 May 2025 23:01:59 +0000 Subject: [PATCH 17/20] Fixed unresolved link --- docs/docs/report/samples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/report/samples.md b/docs/docs/report/samples.md index 47434471c05a..0489e3b92c80 100644 --- a/docs/docs/report/samples.md +++ b/docs/docs/report/samples.md @@ -22,7 +22,7 @@ The following report templates are provided "out of the box" and can be used as | [Sales Order Shipment](#sales-order-shipment) | [SalesOrderShipment](../sales/sales_order.md) | Sales Order Shipment report | | [Stock Location](#stock-location) | [StockLocation](../stock/index.md#stock-location) | Stock Location report | | [Test Report](#test-report) | [StockItem](../stock/index.md#stock-item) | Test Report | -| [Selected Stock Items Report](#selected-stock-items-report) | [StockItem](../stock/stock.md#stock-item) | Selected Stock Items report | +| [Selected Stock Items Report](#selected-stock-items-report) | [StockItem](../stock/index.md#stock-item) | Selected Stock Items report | ### Bill of Materials Report From 026d1d0d6c083dfa7a69090e96f1e292e319af03 Mon Sep 17 00:00:00 2001 From: Tristan Le Date: Wed, 7 May 2025 23:44:15 +0000 Subject: [PATCH 18/20] Updated API version --- src/backend/InvenTree/InvenTree/api_version.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 37bc83361428..4019e5c8a3fd 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,18 +1,18 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 341 +INVENTREE_API_VERSION = 342 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v342 -> 2025-05-07 : https://github.com/inventree/InvenTree/pull/9532 + - Adds "merge" field to the ReportTemplate model + v341 -> 2025-04-21 : https://github.com/inventree/InvenTree/pull/9547 - Require pagination limit on list queries -v341 -> 2025-04-21 : https://github.com/inventree/InvenTree/pull/9532 - - Adds "merge" field to the ReportTemplate model - v340 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9546 - Add nullable to various fields to make them not required From 986407b549ca0163a11f524fe9b3c81f5769e70a Mon Sep 17 00:00:00 2001 From: tristanle22 Date: Mon, 12 May 2025 23:29:07 +0000 Subject: [PATCH 19/20] Fixed test report path issue --- src/backend/InvenTree/report/tests.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index 5c036c4bce28..327e78343530 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -1,8 +1,10 @@ """Unit testing for the various report models.""" +import os from io import StringIO from django.apps import apps +from django.conf import settings from django.core.cache import cache from django.urls import reverse @@ -505,8 +507,10 @@ def test_print(self): # Open and read the output HTML as a string html_report = '' - path_prefix = '/home/inventree/dev' - with open(path_prefix + response.data['output'], encoding='utf-8') as f: + report_path = os.path.join( + settings.MEDIA_ROOT.parent, response.data['output'].lstrip('/') + ) + with open(report_path, encoding='utf-8') as f: html_report = f.read() # Assuming the number of and correlates to the number of pages From 12a04055e06b9d08f2e41d1345c2a91302e429aa Mon Sep 17 00:00:00 2001 From: tristanle22 Date: Sun, 18 May 2025 20:38:36 +0000 Subject: [PATCH 20/20] Add plugin context for each instance --- src/backend/InvenTree/report/models.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/report/models.py b/src/backend/InvenTree/report/models.py index 2aad6dec759e..ebaae55061f1 100644 --- a/src/backend/InvenTree/report/models.py +++ b/src/backend/InvenTree/report/models.py @@ -417,6 +417,11 @@ def get_context(self, instance, request=None, **kwargs): context = {**base_context, **report_context} # Pass the context through to the plugin registry for any additional information + context = self.get_plugin_context(instance, request, context) + return context + + def get_plugin_context(self, instance, request, context): + """Get the context for the plugin.""" for plugin in registry.with_mixin(PluginMixinEnum.REPORT): try: plugin.add_report_context(self, instance, request, context) @@ -503,7 +508,11 @@ def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput: report_context = self.get_report_context() item_contexts = [] for instance in items: - item_contexts.append(instance.report_context()) + instance_context = instance.report_context() + instance_context = self.get_plugin_context( + instance, request, instance_context + ) + item_contexts.append(instance_context) contexts = { **base_context,