Coverage for src/backend/InvenTree/report/apps.py: 69%
93 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Config options for the report app."""
3import logging
4import os
6from django.apps import AppConfig
7from django.core.exceptions import AppRegistryNotReady, ValidationError
8from django.core.files.base import ContentFile
9from django.core.files.storage import default_storage
10from django.db import transaction
11from django.db.utils import IntegrityError, OperationalError, ProgrammingError
13import structlog
14from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode
16import InvenTree.exceptions
17import InvenTree.ready
18from InvenTree.config import get_base_dir
20logger = structlog.getLogger('inventree')
23class ReportConfig(AppConfig):
24 """Configuration class for the "report" app."""
26 name = 'report'
28 def ready(self):
29 """This function is called whenever the app is loaded."""
30 # Configure logging for PDF generation (disable "info" messages)
32 # Reduce log output for fontTools and weasyprint
33 for name, log_manager in logging.root.manager.loggerDict.items():
34 if name.lower().startswith(('fonttools', 'weasyprint')):
35 if hasattr(log_manager, 'setLevel'):
36 log_manager.setLevel(logging.ERROR)
38 super().ready()
40 self.cleanup()
42 # skip loading if plugin registry is not loaded or we run in a background thread
43 if (
44 not InvenTree.ready.isPluginRegistryLoaded()
45 or not InvenTree.ready.isInMainThread()
46 ):
47 return
49 if not InvenTree.ready.canAppAccessDatabase(allow_test=False): 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true
50 return # pragma: no cover
52 if InvenTree.ready.isReadOnlyCommand(): 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 return # pragma: no cover
55 with maintenance_mode_on():
56 try:
57 self.create_default_labels()
58 self.create_default_reports()
59 except ValidationError:
60 logger.warning('Validation error when creating default templates')
61 except (
62 AppRegistryNotReady,
63 IntegrityError,
64 OperationalError,
65 ProgrammingError,
66 ):
67 logger.warning(
68 'Database not ready for creating default report templates'
69 )
71 set_maintenance_mode(False)
73 def cleanup(self):
74 """Cleanup old label and report outputs."""
75 try:
76 from report.tasks import (
77 cleanup_old_report_outputs, # type: ignore[import] # ty: ignore[unresolved-import]
78 )
80 cleanup_old_report_outputs()
81 except Exception:
82 pass
84 def file_from_template(self, dir_name: str, file_name: str) -> ContentFile:
85 """Construct a new ContentFile from a template file.
87 Args:
88 dir_name (str): The name of the directory containing the template
89 file_name (str): The name of the template file
90 Returns:
91 ContentFile: The ContentFile object containing the template
92 """
93 logger.info('Creating %s template file: %s', dir_name, file_name)
94 file = get_base_dir().joinpath('report', 'templates', dir_name, file_name)
95 if not file.exists(): 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 raise FileNotFoundError(
97 'Template file %s does not exist in %s', file_name, file
98 )
99 return ContentFile(file.open('r').read(), os.path.basename(file_name))
101 def create_default_labels(self):
102 """Create default label templates."""
103 # Test if models are ready
104 try:
105 import report.models
106 except Exception: # pragma: no cover
107 # Database is not ready yet
108 return
110 assert bool(report.models.LabelTemplate is not None)
112 label_templates = [
113 {
114 'file': 'part_label.html',
115 'name': 'InvenTree Part Label',
116 'description': 'Sample part label',
117 'model_type': 'part',
118 },
119 {
120 'file': 'part_label_code128.html',
121 'name': 'InvenTree Part Label (Code128)',
122 'description': 'Sample part label with Code128 barcode',
123 'model_type': 'part',
124 },
125 {
126 'file': 'stockitem_qr.html',
127 'name': 'InvenTree Stock Item Label (QR)',
128 'description': 'Sample stock item label with QR code',
129 'model_type': 'stockitem',
130 },
131 {
132 'file': 'stocklocation_qr_and_text.html',
133 'name': 'InvenTree Stock Location Label (QR + Text)',
134 'description': 'Sample stock item label with QR code and text',
135 'model_type': 'stocklocation',
136 },
137 {
138 'file': 'stocklocation_qr.html',
139 'name': 'InvenTree Stock Location Label (QR)',
140 'description': 'Sample stock location label with QR code',
141 'model_type': 'stocklocation',
142 },
143 {
144 'file': 'buildline_label.html',
145 'name': 'InvenTree Build Line Label',
146 'description': 'Sample build line label',
147 'model_type': 'buildline',
148 },
149 ]
151 for template in label_templates:
152 filename = template.pop('file')
154 # Template already exists in the database - check that the file exists too
155 if existing_template := report.models.LabelTemplate.objects.filter( 155 ↛ 158line 155 didn't jump to line 158 because the condition on line 155 was never true
156 name=template['name'], model_type=template['model_type']
157 ).first():
158 if not default_storage.exists(existing_template.template.name):
159 # The file does not exist in the storage system - add it in
160 existing_template.template = self.file_from_template(
161 'label', filename
162 )
163 existing_template.save()
164 continue
166 # Otherwise, create a new entry
167 try:
168 # Create a new entry
169 with transaction.atomic():
170 report.models.LabelTemplate.objects.create(
171 **template, template=self.file_from_template('label', filename)
172 )
173 logger.info("Creating new label template: '%s'", template['name'])
174 except ValidationError:
175 logger.warning(
176 "Could not create label template: '%s'", template['name']
177 )
178 except Exception:
179 InvenTree.exceptions.log_error('create_default_labels', scope='init')
181 def create_default_reports(self):
182 """Create default report templates."""
183 # Test if models are ready
184 try:
185 import report.models
186 except Exception: # pragma: no cover
187 # Database is not ready yet
188 return
190 assert bool(report.models.ReportTemplate is not None)
192 # Construct a set of default ReportTemplate instances
193 report_templates = [
194 {
195 'file': 'inventree_bill_of_materials_report.html',
196 'name': 'InvenTree Bill of Materials',
197 'description': 'Sample bill of materials report',
198 'model_type': 'part',
199 },
200 {
201 'file': 'inventree_build_order_report.html',
202 'name': 'InvenTree Build Order',
203 'description': 'Sample build order report',
204 'model_type': 'build',
205 },
206 {
207 'file': 'inventree_purchase_order_report.html',
208 'name': 'InvenTree Purchase Order',
209 'description': 'Sample purchase order report',
210 'model_type': 'purchaseorder',
211 'filename_pattern': 'PurchaseOrder-{{ reference }}.pdf',
212 },
213 {
214 'file': 'inventree_sales_order_report.html',
215 'name': 'InvenTree Sales Order',
216 'description': 'Sample sales order report',
217 'model_type': 'salesorder',
218 'filename_pattern': 'SalesOrder-{{ reference }}.pdf',
219 },
220 {
221 'file': 'inventree_sales_order_shipment_report.html',
222 'name': 'InvenTree Sales Order Shipment',
223 'description': 'Sample sales order shipment report',
224 'model_type': 'salesordershipment',
225 'filename_pattern': 'SalesOrderShipment-{{ reference }}.pdf',
226 },
227 {
228 'file': 'inventree_return_order_report.html',
229 'name': 'InvenTree Return Order',
230 'description': 'Sample return order report',
231 'model_type': 'returnorder',
232 'filename_pattern': 'ReturnOrder-{{ reference }}.pdf',
233 },
234 {
235 'file': 'inventree_transfer_order_report.html',
236 'name': 'InvenTree Transfer Order',
237 'description': 'Sample transfer order report',
238 'model_type': 'transferorder',
239 'filename_pattern': 'TransferOrder-{{ reference }}.pdf',
240 },
241 {
242 'file': 'inventree_test_report.html',
243 'name': 'InvenTree Test Report',
244 'description': 'Sample stock item test report',
245 'model_type': 'stockitem',
246 },
247 {
248 'file': 'inventree_stock_report_merge.html',
249 'name': 'InvenTree Default Stock Report Merge',
250 'description': 'Sample stock item report merge',
251 'model_type': 'stockitem',
252 'merge': True,
253 },
254 {
255 'file': 'inventree_stock_location_report.html',
256 'name': 'InvenTree Stock Location Report',
257 'description': 'Sample stock location report',
258 'model_type': 'stocklocation',
259 },
260 ]
262 for template in report_templates:
263 filename = template.pop('file')
265 # Template already exists in the database - check that the file exists too
266 if existing_template := report.models.ReportTemplate.objects.filter( 266 ↛ 269line 266 didn't jump to line 269 because the condition on line 266 was never true
267 name=template['name'], model_type=template['model_type']
268 ).first():
269 if not default_storage.exists(existing_template.template.name):
270 # The file does not exist in the storage system - add it in
271 existing_template.template = self.file_from_template(
272 'report', filename
273 )
275 existing_template.save()
276 continue
278 # Otherwise, create a new entry
279 try:
280 report.models.ReportTemplate.objects.create(
281 **template, template=self.file_from_template('report', filename)
282 )
283 logger.info("Created new report template: '%s'", template['name'])
284 except ValidationError:
285 logger.warning(
286 "Could not create report template: '%s'", template['name']
287 )
288 except Exception:
289 InvenTree.exceptions.log_error('create_default_reports', scope='init')