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 all 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
2 changes: 2 additions & 0 deletions docs/docs/report/context_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/report/samples.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ 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/index.md#stock-item) | Selected Stock Items report |


### Bill of Materials Report

Expand Down Expand Up @@ -55,6 +57,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:
Expand Down
6 changes: 5 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 342
INVENTREE_API_VERSION = 343

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


INVENTREE_API_TEXT = """

v343 -> 2025-05-10 : https://github.com/inventree/InvenTree/pull/9532
- Adds "merge" field to the ReportTemplate model

v342 -> 2025-05-09 : https://github.com/inventree/InvenTree/pull/9651
- Fix serializer to match Generate API for serial numbers

Expand Down
7 changes: 7 additions & 0 deletions src/backend/InvenTree/report/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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", "0030_alter_labeltemplate_model_type_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",
),
),
]
134 changes: 101 additions & 33 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: Django template language contexts (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: Django template langaguage contexts (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,17 +399,29 @@ 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
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)
Expand All @@ -409,6 +432,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,8 +484,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(
Expand All @@ -461,46 +503,72 @@ 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:
instance_context = instance.report_context()
instance_context = self.get_plugin_context(
instance, request, instance_context
)
item_contexts.append(instance_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
Loading