Skip to content

Report merge #9532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a92a1e1
Report merge
tristanle22 Apr 18, 2025
bd57809
Merge remote-tracking branch 'origin/master' into report_merge
tristanle22 Apr 18, 2025
25eef24
Remove auto-generated file
tristanle22 Apr 18, 2025
00d80ba
Remove pre-commit file
tristanle22 Apr 18, 2025
1c43864
Revert "Remove pre-commit file"
tristanle22 Apr 18, 2025
b4ce88b
Update API version
tristanle22 Apr 18, 2025
3f22396
Merge remote-tracking branch 'origin/master' into report_merge
tristanle22 Apr 20, 2025
5f58cf5
Reduced duplicated logic
tristanle22 Apr 21, 2025
559396e
Merge branch 'master' into report_merge
matmair Apr 21, 2025
5fb3638
reset pre-commit config
matmair Apr 21, 2025
497706e
Added migration files
tristanle22 Apr 23, 2025
b5b9397
Merge branch 'report_merge' of https://github.com/tristanle22/InvenTr…
tristanle22 Apr 23, 2025
7ef03b0
Added unit test
tristanle22 Apr 24, 2025
02a9af8
Removed redundant migration
tristanle22 Apr 24, 2025
4299338
Updated migration file
tristanle22 Apr 30, 2025
aeda554
Added a default report template with merge enabled
tristanle22 Apr 30, 2025
e4eb544
Unit test to ensure a single page is generated
tristanle22 Apr 30, 2025
effd432
Added docs to support merge feature
tristanle22 May 1, 2025
ae979e1
Clean up
tristanle22 May 1, 2025
0c11cb7
Merge branch 'master' into report_merge
tristanle22 May 1, 2025
390f03e
Merge branch 'report_merge' of https://github.com/tristanle22/InvenTr…
tristanle22 May 1, 2025
33bbc8b
Clean up
tristanle22 May 1, 2025
579e627
Merge branch 'master' into report_merge
tristanle22 May 5, 2025
dcb1040
Fixed unresolved link
tristanle22 May 5, 2025
da5a113
Merge branch 'report_merge' of https://github.com/tristanle22/InvenTr…
tristanle22 May 5, 2025
b9bdcfe
Merge branch 'master' into report_merge
tristanle22 May 7, 2025
026d1d0
Updated API version
tristanle22 May 7, 2025
f441996
Merge branch 'master' into report_merge
tristanle22 May 9, 2025
c5eb102
Merge branch 'master' into report_merge
tristanle22 May 10, 2025
986407b
Fixed test report path issue
tristanle22 May 12, 2025
d65292b
Merge branch 'report_merge' of https://github.com/tristanle22/InvenTr…
tristanle22 May 12, 2025
306adf2
Merge branch 'master' into report_merge
tristanle22 May 12, 2025
d20cf41
Merge branch 'master' into report_merge
tristanle22 May 15, 2025
12a0405
Add plugin context for each instance
tristanle22 May 18, 2025
56fb14c
Merge branch 'report_merge' of https://github.com/tristanle22/InvenTr…
tristanle22 May 18, 2025
33af985
Merge branch 'master' into report_merge
tristanle22 May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 339
INVENTREE_API_VERSION = 340

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """

v340 -> 2025-04-18 : https://github.com/inventree/InvenTree/pull/9532
- Adds "merge" field to the ReportTemplate model

v339 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9283
- Remove need for source in /plugins/ui/features

Expand Down
125 changes: 93 additions & 32 deletions src/backend/InvenTree/report/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -231,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
Expand Down Expand Up @@ -372,6 +377,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:
Expand All @@ -388,14 +399,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
Expand All @@ -409,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.

Expand Down Expand Up @@ -440,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:
Expand All @@ -461,46 +500,68 @@ 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)
report_name = self.generate_filename(contexts)

# Render the report output
try:
if debug_mode:
report = self.render_as_string(instance, request)
report = self.render_as_string(instance, request, contexts)
else:
report = self.render(instance, request)
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
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, None)
else:
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)

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
output.save()

except Exception as exc:
# Something went wrong during the report generation process
Expand Down
7 changes: 6 additions & 1 deletion src/backend/InvenTree/report/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ function ReportTemplateTable() {
<YesNoButton value={instance.landscape} />
)
},
merge: {
label: t`Merge`,
modelRenderer: (instance: any) => (
<YesNoButton value={instance.merge} />
)
},
attach_to_model: {
label: t`Attach to Model`,
modelRenderer: (instance: any) => (
Expand Down