Coverage for src/backend/InvenTree/importer/operations.py: 16%

45 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""Data import operational functions.""" 

2 

3from typing import Optional 

4 

5from django.core.exceptions import ValidationError 

6from django.utils.translation import gettext_lazy as _ 

7 

8import tablib 

9import tablib.core 

10 

11import InvenTree.helpers 

12 

13 

14def load_data_file(data_file, file_format=None): 

15 """Load data file into a tablib dataset. 

16 

17 Arguments: 

18 data_file: django file object containing data to import (should be already opened!) 

19 file_format: Format specifier for the data file 

20 """ 

21 # Introspect the file format based on the provided file 

22 if not file_format: 

23 file_format = data_file.name.split('.')[-1] 

24 

25 if file_format and file_format.startswith('.'): 

26 file_format = file_format[1:] 

27 

28 file_format = file_format.strip().lower() 

29 

30 if file_format not in InvenTree.helpers.GetExportFormats(): 

31 raise ValidationError(_('Unsupported data file format')) 

32 

33 file_object = data_file.file 

34 

35 if hasattr(file_object, 'open'): 

36 file_object.open('r') 

37 

38 file_object.seek(0) 

39 

40 try: 

41 data = file_object.read() 

42 except OSError: 

43 raise ValidationError(_('Failed to open data file')) 

44 

45 # Excel formats expect binary data 

46 if file_format not in ['xls', 'xlsx']: 

47 data = data.decode() 

48 

49 try: 

50 data = tablib.Dataset().load(data, headers=True, format=file_format) 

51 except tablib.core.UnsupportedFormat: 

52 raise ValidationError(_('Unsupported data file format')) 

53 except tablib.core.InvalidDimensions: 

54 raise ValidationError(_('Invalid data file dimensions')) 

55 

56 return data 

57 

58 

59def extract_column_names(data_file) -> list: 

60 """Extract column names from a data file. 

61 

62 Uses the tablib library to extract column names from a data file. 

63 

64 Args: 

65 data_file: File object containing data to import 

66 

67 Returns: 

68 List of column names extracted from the file 

69 

70 Raises: 

71 ValidationError: If the data file is not in a valid format 

72 """ 

73 data = load_data_file(data_file) 

74 

75 return normalize_headers(data.headers) 

76 

77 

78def normalize_headers(headers) -> list: 

79 """Normalize a list of raw column headers extracted from a data file. 

80 

81 Strips whitespace from each header, and generates a default header 

82 for any column that does not have one. Must be used consistently 

83 wherever column headers are extracted, so that column names used for 

84 field mapping match the column names used when extracting row data. 

85 

86 Args: 

87 headers: List of raw header values (as returned by tablib) 

88 

89 Returns: 

90 List of normalized column names 

91 """ 

92 result = [] 

93 

94 for idx, header in enumerate(headers): 

95 if header: 

96 result.append(str(header).strip()) 

97 else: 

98 # If the header is empty, generate a default header 

99 result.append(f'Column {idx + 1}') 

100 

101 return result 

102 

103 

104def get_field_label(field) -> Optional[str]: 

105 """Return the label for a field in a serializer class. 

106 

107 Check for labels in the following order of descending priority: 

108 

109 - The serializer class has a 'label' specified for the field 

110 - The underlying model has a 'verbose_name' specified 

111 - The field name is used as the label 

112 

113 Arguments: 

114 field: Field instance from a serializer class 

115 

116 Returns: 

117 str: Field label 

118 """ 

119 if field and (label := getattr(field, 'label', None)): 

120 return label 

121 

122 # TODO: Check if the field is a model field 

123 

124 return None