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

1"""Config options for the report app.""" 

2 

3import logging 

4import os 

5 

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 

12 

13import structlog 

14from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode 

15 

16import InvenTree.exceptions 

17import InvenTree.ready 

18from InvenTree.config import get_base_dir 

19 

20logger = structlog.getLogger('inventree') 

21 

22 

23class ReportConfig(AppConfig): 

24 """Configuration class for the "report" app.""" 

25 

26 name = 'report' 

27 

28 def ready(self): 

29 """This function is called whenever the app is loaded.""" 

30 # Configure logging for PDF generation (disable "info" messages) 

31 

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) 

37 

38 super().ready() 

39 

40 self.cleanup() 

41 

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 

48 

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 

51 

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 

54 

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 ) 

70 

71 set_maintenance_mode(False) 

72 

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 ) 

79 

80 cleanup_old_report_outputs() 

81 except Exception: 

82 pass 

83 

84 def file_from_template(self, dir_name: str, file_name: str) -> ContentFile: 

85 """Construct a new ContentFile from a template file. 

86 

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)) 

100 

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 

109 

110 assert bool(report.models.LabelTemplate is not None) 

111 

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 ] 

150 

151 for template in label_templates: 

152 filename = template.pop('file') 

153 

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 

165 

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') 

180 

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 

189 

190 assert bool(report.models.ReportTemplate is not None) 

191 

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 ] 

261 

262 for template in report_templates: 

263 filename = template.pop('file') 

264 

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 ) 

274 

275 existing_template.save() 

276 continue 

277 

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')