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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Sample supplier plugin."""
3from django.conf import settings
5from company.models import Company, ManufacturerPart, SupplierPart, SupplierPriceBreak
6from part.models import Part
7from plugin.mixins import SupplierMixin, supplier
8from plugin.plugin import InvenTreePlugin
11class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin):
12 """Example plugin to integrate with a dummy supplier."""
14 NAME = 'SampleSupplierPlugin'
15 SLUG = 'samplesupplier'
16 TITLE = 'My sample supplier plugin'
18 VERSION = '0.0.2'
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 }
29 def __init__(self):
30 """Initialize the sample supplier plugin."""
31 super().__init__()
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 })
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')]
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 ]
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
92 raise supplier.PartNotFoundError()
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']
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 ]
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 )
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)
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 )
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()
158 return part
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 )
171 mft_part, created = ManufacturerPart.objects.get_or_create(
172 MPN=f'MAN-{data["sku"]}', manufacturer=mft, **kwargs
173 )
175 if created:
176 # Attachments, notes, parameters and more can be added here
177 pass
179 return mft_part
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 )
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 ])
198 return spp