Coverage for src/backend/InvenTree/plugin/samples/supplier/supplier_sample.py: 0%

58 statements  

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

1"""Sample supplier plugin.""" 

2 

3from django.conf import settings 

4 

5from company.models import Company, ManufacturerPart, SupplierPart, SupplierPriceBreak 

6from part.models import Part 

7from plugin.mixins import SupplierMixin, supplier 

8from plugin.plugin import InvenTreePlugin 

9 

10 

11class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin): 

12 """Example plugin to integrate with a dummy supplier.""" 

13 

14 NAME = 'SampleSupplierPlugin' 

15 SLUG = 'samplesupplier' 

16 TITLE = 'My sample supplier plugin' 

17 

18 VERSION = '0.0.2' 

19 

20 SETTINGS = { 

21 'DOWNLOAD_IMAGES': { 

22 'name': 'Download part images', 

23 'description': 'Enable downloading of part images during import (not recommended during testing)', 

24 'validator': bool, 

25 'default': False, 

26 } 

27 } 

28 

29 def __init__(self): 

30 """Initialize the sample supplier plugin.""" 

31 super().__init__() 

32 

33 self.sample_data = [] 

34 for material in ['Steel', 'Aluminium', 'Brass']: 

35 for size in ['M1', 'M2', 'M3', 'M4', 'M5']: 

36 for length in range(5, 30, 5): 

37 self.sample_data.append({ 

38 'material': material, 

39 'thread': size, 

40 'length': length, 

41 'sku': f'BOLT-{material}-{size}-{length}', 

42 'name': f'Bolt {size}x{length}mm {material}', 

43 'description': f'This is a sample part description demonstration purposes for the {size}x{length} {material} bolt.', 

44 'price': { 

45 1: [1.0, 'EUR'], 

46 10: [0.9, 'EUR'], 

47 100: [0.8, 'EUR'], 

48 5000: [0.5, 'EUR'], 

49 }, 

50 'link': f'https://example.com/sample-part-{size}-{length}-{material}', 

51 'image_url': r'https://github.com/inventree/demo-dataset/blob/main/media/part_images/flat-head.png?raw=true', 

52 'brand': 'Bolt Manufacturer', 

53 }) 

54 

55 def get_suppliers(self) -> list[supplier.Supplier]: 

56 """Return a list of available suppliers.""" 

57 return [supplier.Supplier(slug='sample-fasteners', name='Sample Fasteners')] 

58 

59 def get_search_results( 

60 self, supplier_slug: str, term: str 

61 ) -> list[supplier.SearchResult]: 

62 """Return a list of search results based on the search term.""" 

63 return [ 

64 supplier.SearchResult( 

65 sku=p['sku'], 

66 name=p['name'], 

67 description=p['description'], 

68 exact=p['sku'] == term, 

69 price=f'{p["price"][1][0]:.2f}€', # ty:ignore[not-subscriptable] 

70 link=p['link'], 

71 image_url=p['image_url'], 

72 existing_part=getattr( 

73 SupplierPart.objects.filter(SKU=p['sku']).first(), 'part', None 

74 ), 

75 ) 

76 for p in self.sample_data 

77 if all(t.lower() in p['name'].lower() for t in term.split()) 

78 ] 

79 

80 def get_import_data(self, supplier_slug: str, part_id: str): 

81 """Return import data for a specific part ID.""" 

82 for p in self.sample_data: 

83 if p['sku'] == part_id: 

84 p = p.copy() 

85 p['variants'] = [ 

86 x['sku'] 

87 for x in self.sample_data 

88 if x['thread'] == p['thread'] and x['length'] == p['length'] 

89 ] 

90 return p 

91 

92 raise supplier.PartNotFoundError() 

93 

94 def get_pricing_data(self, data) -> dict[int, tuple[float, str]]: 

95 """Return pricing data for the given part data.""" 

96 return data['price'] 

97 

98 def get_parameters(self, data) -> list[supplier.ImportParameter]: 

99 """Return a list of parameters for the given part data.""" 

100 return [ 

101 supplier.ImportParameter(name='Thread', value=data['thread'][1:]), 

102 supplier.ImportParameter(name='Length', value=f'{data["length"]}mm'), 

103 supplier.ImportParameter(name='Material', value=data['material']), 

104 supplier.ImportParameter(name='Head', value='Flat Head'), 

105 ] 

106 

107 def import_part(self, data, **kwargs) -> Part: 

108 """Import a part based on the provided data.""" 

109 part, created = Part.objects.get_or_create( 

110 name__iexact=data['sku'], 

111 purchaseable=True, 

112 defaults={ 

113 'name': data['sku'], 

114 'description': data['description'], 

115 'link': data['link'], 

116 **kwargs, 

117 }, 

118 ) 

119 

120 # If the part was created, set additional fields 

121 if created: 

122 # Prevent downloading images during testing, as this can lead to unreliable tests 

123 if ( 

124 data['image_url'] 

125 and not settings.TESTING 

126 and self.get_setting('DOWNLOAD_IMAGES') 

127 ): 

128 file, fmt = self.download_image(data['image_url']) 

129 filename = f'part_{part.pk}_image.{fmt.lower()}' 

130 part.image.save(filename, file) 

131 

132 # link other variants if they exist in our inventree database 

133 if len(data['variants']): 

134 # search for other parts that may already have a template part associated 

135 variant_parts = [ 

136 x.part 

137 for x in SupplierPart.objects.filter(SKU__in=data['variants']) 

138 ] 

139 parent_part = self.get_template_part( 

140 variant_parts, 

141 { 

142 # we cannot extract a real name for the root part, but we can try to guess a unique name 

143 'name': data['sku'].replace(data['material'] + '-', ''), 

144 'description': data['name'].replace(' ' + data['material'], ''), 

145 'link': data['link'], 

146 'image': part.image.name, 

147 'is_template': True, 

148 **kwargs, 

149 }, 

150 ) 

151 

152 # after the template part was created, we need to refresh the part from the db because its tree id may have changed 

153 # which results in an error if saved directly 

154 part.refresh_from_db() 

155 part.variant_of = parent_part 

156 part.save() 

157 

158 return part 

159 

160 def import_manufacturer_part(self, data, **kwargs) -> ManufacturerPart: 

161 """Import a manufacturer part based on the provided data.""" 

162 mft, _ = Company.objects.get_or_create( 

163 name__iexact=data['brand'], 

164 defaults={ 

165 'is_manufacturer': True, 

166 'is_supplier': False, 

167 'name': data['brand'], 

168 }, 

169 ) 

170 

171 mft_part, created = ManufacturerPart.objects.get_or_create( 

172 MPN=f'MAN-{data["sku"]}', manufacturer=mft, **kwargs 

173 ) 

174 

175 if created: 

176 # Attachments, notes, parameters and more can be added here 

177 pass 

178 

179 return mft_part 

180 

181 def import_supplier_part(self, data, **kwargs) -> SupplierPart: 

182 """Import a supplier part based on the provided data.""" 

183 spp, _ = SupplierPart.objects.get_or_create( 

184 SKU=data['sku'], 

185 supplier=self.supplier_company, 

186 **kwargs, 

187 defaults={'link': data['link']}, 

188 ) 

189 

190 SupplierPriceBreak.objects.filter(part=spp).delete() 

191 SupplierPriceBreak.objects.bulk_create([ 

192 SupplierPriceBreak( 

193 part=spp, quantity=quantity, price=price, price_currency=currency 

194 ) 

195 for quantity, (price, currency) in data['price'].items() 

196 ]) 

197 

198 return spp