Coverage for src/backend/InvenTree/company/api.py: 96%
140 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"""Provides a JSON API for the Company app."""
3from django.db.models import Q
4from django.urls import include, path
5from django.utils.translation import gettext_lazy as _
7import django_filters.rest_framework.filters as rest_filters
8from django_filters.rest_framework.filterset import FilterSet
10import common.filters
11import part.models
12from data_exporter.mixins import DataExportViewMixin
13from InvenTree.api import ListCreateDestroyAPIView, ParameterListMixin, meta_path
14from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
15from InvenTree.filters import SEARCH_ORDER_FILTER
16from InvenTree.mixins import (
17 ListCreateAPI,
18 OutputOptionsMixin,
19 RetrieveUpdateDestroyAPI,
20 SerializerContextMixin,
21)
23from .models import (
24 Address,
25 Company,
26 Contact,
27 ManufacturerPart,
28 SupplierPart,
29 SupplierPriceBreak,
30)
31from .serializers import (
32 AddressSerializer,
33 CompanySerializer,
34 ContactSerializer,
35 ManufacturerPartSerializer,
36 SupplierPartSerializer,
37 SupplierPriceBreakSerializer,
38)
41class CompanyFilter(FilterSet):
42 """Custom API filters for the CompanyList endpoint."""
44 class Meta:
45 """Metaclass options."""
47 model = Company
48 fields = ['is_customer', 'is_manufacturer', 'is_supplier', 'name', 'active']
50 tags = common.filters.TagsFilter()
53class CompanyMixin(OutputOptionsMixin):
54 """Mixin class for Company API endpoints."""
56 queryset = Company.objects.all()
57 serializer_class = CompanySerializer
59 def get_queryset(self):
60 """Return annotated queryset for the company endpoints."""
61 queryset = super().get_queryset()
62 queryset = CompanySerializer.annotate_queryset(queryset)
64 return queryset
67class CompanyList(CompanyMixin, ParameterListMixin, DataExportViewMixin, ListCreateAPI):
68 """API endpoint for accessing a list of Company objects.
70 Provides two methods:
72 - GET: Return list of objects
73 - POST: Create a new Company object
74 """
76 filter_backends = SEARCH_ORDER_FILTER
78 filterset_class = CompanyFilter
80 search_fields = ['name', 'description', 'website', 'tax_id']
82 ordering_fields = ['active', 'name', 'parts_supplied', 'parts_manufactured']
84 ordering = 'name'
87class CompanyDetail(CompanyMixin, RetrieveUpdateDestroyAPI):
88 """API endpoint for detail of a single Company object."""
91class ContactList(DataExportViewMixin, ListCreateDestroyAPIView):
92 """API endpoint for list view of Company model."""
94 queryset = Contact.objects.all()
95 serializer_class = ContactSerializer
97 filter_backends = SEARCH_ORDER_FILTER
99 filterset_fields = ['company']
101 search_fields = ['company__name', 'name']
103 ordering_fields = ['name']
105 ordering = 'name'
108class ContactDetail(RetrieveUpdateDestroyAPI):
109 """Detail endpoint for Company model."""
111 queryset = Contact.objects.all()
112 serializer_class = ContactSerializer
115class AddressList(DataExportViewMixin, ListCreateDestroyAPIView):
116 """API endpoint for list view of Address model."""
118 queryset = Address.objects.all()
119 serializer_class = AddressSerializer
121 filter_backends = SEARCH_ORDER_FILTER
123 filterset_fields = ['company']
125 ordering_fields = ['title']
127 ordering = 'title'
130class AddressDetail(RetrieveUpdateDestroyAPI):
131 """API endpoint for a single Address object."""
133 queryset = Address.objects.all()
134 serializer_class = AddressSerializer
137class ManufacturerPartFilter(FilterSet):
138 """Custom API filters for the ManufacturerPart list endpoint."""
140 class Meta:
141 """Metaclass options."""
143 model = ManufacturerPart
144 fields = ['manufacturer', 'MPN', 'part', 'tags__name', 'tags__slug']
146 # Filter by 'active' status of linked part
147 part_active = rest_filters.BooleanFilter(
148 field_name='part__active', label=_('Part is Active')
149 )
151 manufacturer_active = rest_filters.BooleanFilter(
152 field_name='manufacturer__active', label=_('Manufacturer is Active')
153 )
155 tags = common.filters.TagsFilter(label=_('Tags'))
158class ManufacturerOutputOptions(OutputConfiguration):
159 """Available output options for the ManufacturerPart endpoints."""
161 OPTIONS = [
162 InvenTreeOutputOption(
163 description='Include detailed information about the linked Part in the response',
164 flag='part_detail',
165 default=False,
166 ),
167 InvenTreeOutputOption(
168 description='Include detailed information about the Manufacturer in the response',
169 flag='manufacturer_detail',
170 default=False,
171 ),
172 InvenTreeOutputOption(
173 description='Format the output with a more readable (pretty) name',
174 flag='pretty',
175 default=False,
176 ),
177 ]
180class ManufacturerPartMixin(SerializerContextMixin):
181 """Mixin class for ManufacturerPart API endpoints."""
183 queryset = ManufacturerPart.objects.all()
184 serializer_class = ManufacturerPartSerializer
186 def get_queryset(self, *args, **kwargs):
187 """Return annotated queryset for the ManufacturerPart list endpoint."""
188 queryset = super().get_queryset(*args, **kwargs)
190 queryset = queryset.prefetch_related('supplier_parts')
191 queryset = queryset.prefetch_related('part', 'part__pricing_data')
193 return queryset
196class ManufacturerPartList(
197 DataExportViewMixin,
198 ManufacturerPartMixin,
199 SerializerContextMixin,
200 OutputOptionsMixin,
201 ParameterListMixin,
202 ListCreateDestroyAPIView,
203):
204 """API endpoint for list view of ManufacturerPart object.
206 - GET: Return list of ManufacturerPart objects
207 - POST: Create a new ManufacturerPart object
208 """
210 filterset_class = ManufacturerPartFilter
211 filter_backends = SEARCH_ORDER_FILTER
212 output_options = ManufacturerOutputOptions
214 ordering_fields = ['part', 'IPN', 'MPN', 'manufacturer']
216 ordering_field_aliases = {
217 'part': 'part__name',
218 'IPN': 'part__IPN',
219 'manufacturer': 'manufacturer__name',
220 }
222 search_fields = [
223 'manufacturer__name',
224 'description',
225 'MPN',
226 'part__IPN',
227 'part__name',
228 'part__description',
229 'tags__name',
230 'tags__slug',
231 ]
234class ManufacturerPartDetail(
235 ManufacturerPartMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
236):
237 """API endpoint for detail view of ManufacturerPart object.
239 - GET: Retrieve detail view
240 - PATCH: Update object
241 - DELETE: Delete object
242 """
245class SupplierPartFilter(FilterSet):
246 """API filters for the SupplierPartList endpoint."""
248 class Meta:
249 """Metaclass option."""
251 model = SupplierPart
252 fields = [
253 'supplier',
254 'part',
255 'manufacturer_part',
256 'SKU',
257 'tags__name',
258 'tags__slug',
259 ]
261 active = rest_filters.BooleanFilter(label=_('Supplier Part is Active'))
263 primary = rest_filters.BooleanFilter(label=_('Primary Supplier Part'))
265 # Filter by 'active' status of linked part
266 part_active = rest_filters.BooleanFilter(
267 field_name='part__active', label=_('Internal Part is Active')
268 )
270 # Filter by 'active' status of linked supplier
271 supplier_active = rest_filters.BooleanFilter(
272 field_name='supplier__active', label=_('Supplier is Active')
273 )
275 # Filter by the 'MPN' of linked manufacturer part
276 MPN = rest_filters.CharFilter(
277 label='Manufacturer Part Number',
278 field_name='manufacturer_part__MPN',
279 lookup_expr='iexact',
280 )
282 # Filter by 'manufacturer'
283 manufacturer = rest_filters.ModelChoiceFilter(
284 label=_('Manufacturer'),
285 queryset=Company.objects.all(),
286 field_name='manufacturer_part__manufacturer',
287 )
289 # Filter by 'company' (either manufacturer or supplier)
290 company = rest_filters.ModelChoiceFilter(
291 label=_('Company'), queryset=Company.objects.all(), method='filter_company'
292 )
294 def filter_company(self, queryset, name, value: int):
295 """Filter the queryset by either manufacturer or supplier."""
296 return queryset.filter(
297 Q(manufacturer_part__manufacturer=value) | Q(supplier=value)
298 ).distinct()
300 has_stock = rest_filters.BooleanFilter(
301 label=_('Has Stock'), method='filter_has_stock'
302 )
304 def filter_has_stock(self, queryset, name, value):
305 """Filter the queryset based on whether the SupplierPart has stock available."""
306 if value:
307 return queryset.filter(in_stock__gt=0)
308 else:
309 return queryset.exclude(in_stock__gt=0)
311 tags = common.filters.TagsFilter(label=_('Tags'))
314class SupplierPartOutputOptions(OutputConfiguration):
315 """Available output options for the SupplierPart endpoints."""
317 OPTIONS = [
318 InvenTreeOutputOption(
319 description='Include detailed information about the linked Part in the response',
320 flag='part_detail',
321 default=False,
322 ),
323 InvenTreeOutputOption(
324 description='Include detailed information about the Supplier in the response',
325 flag='supplier_detail',
326 default=False,
327 ),
328 InvenTreeOutputOption(
329 description='Include detailed information about the Manufacturer in the response',
330 flag='manufacturer_detail',
331 default=False,
332 ),
333 InvenTreeOutputOption(
334 flag='manufacturer_part_detail',
335 description='Include detailed information about the linked ManufacturerPart in the response',
336 default=False,
337 ),
338 InvenTreeOutputOption(
339 description='Format the output with a more readable (pretty) name',
340 flag='pretty',
341 default=False,
342 ),
343 ]
346class SupplierPartMixin:
347 """Mixin class for SupplierPart API endpoints."""
349 queryset = SupplierPart.objects.all()
350 serializer_class = SupplierPartSerializer
352 def get_queryset(self, *args, **kwargs):
353 """Return annotated queryset object for the SupplierPart list."""
354 queryset = super().get_queryset(*args, **kwargs)
355 queryset = SupplierPartSerializer.annotate_queryset(queryset)
357 queryset = queryset.prefetch_related('part', 'part__pricing_data')
359 return queryset
362class SupplierPartList(
363 DataExportViewMixin,
364 SupplierPartMixin,
365 ParameterListMixin,
366 OutputOptionsMixin,
367 ListCreateDestroyAPIView,
368):
369 """API endpoint for list view of SupplierPart object.
371 - GET: Return list of SupplierPart objects
372 - POST: Create a new SupplierPart object
373 """
375 filterset_class = SupplierPartFilter
376 filter_backends = SEARCH_ORDER_FILTER
377 output_options = SupplierPartOutputOptions
379 ordering_fields = [
380 'part',
381 'supplier',
382 'manufacturer',
383 'active',
384 'primary',
385 'IPN',
386 'MPN',
387 'SKU',
388 'packaging',
389 'pack_quantity',
390 'in_stock',
391 'updated',
392 ]
394 ordering_field_aliases = {
395 'part': 'part__name',
396 'supplier': 'supplier__name',
397 'manufacturer': 'manufacturer_part__manufacturer__name',
398 'pack_quantity': ['pack_quantity_native', 'pack_quantity'],
399 'IPN': 'part__IPN',
400 'MPN': 'manufacturer_part__MPN',
401 }
403 search_fields = [
404 'SKU',
405 'supplier__name',
406 'manufacturer_part__manufacturer__name',
407 'description',
408 'manufacturer_part__MPN',
409 'part__IPN',
410 'part__name',
411 'part__description',
412 'part__keywords',
413 'tags__name',
414 'tags__slug',
415 ]
418class SupplierPartDetail(
419 SupplierPartMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
420):
421 """API endpoint for detail view of SupplierPart object.
423 - GET: Retrieve detail view
424 - PATCH: Update object
425 - DELETE: Delete object
426 """
428 output_options = SupplierPartOutputOptions
431class SupplierPriceBreakFilter(FilterSet):
432 """Custom API filters for the SupplierPriceBreak list endpoint."""
434 class Meta:
435 """Metaclass options."""
437 model = SupplierPriceBreak
438 fields = ['part', 'quantity']
440 base_part = rest_filters.ModelChoiceFilter(
441 label='Base Part',
442 queryset=part.models.Part.objects.all(),
443 field_name='part__part',
444 )
446 supplier = rest_filters.ModelChoiceFilter(
447 label='Supplier', queryset=Company.objects.all(), field_name='part__supplier'
448 )
451class SupplierPriceBreakMixin:
452 """Mixin class for SupplierPriceBreak API endpoints."""
454 queryset = SupplierPriceBreak.objects.all()
455 serializer_class = SupplierPriceBreakSerializer
458class SupplierPriceBreakOutputOptions(OutputConfiguration):
459 """Available output options for the SupplierPriceBreak endpoints."""
461 OPTIONS = [
462 InvenTreeOutputOption(
463 description='Include detailed information about the linked Part in the response',
464 flag='part_detail',
465 default=False,
466 ),
467 InvenTreeOutputOption(
468 description='Include detailed information about the Supplier in the response',
469 flag='supplier_detail',
470 default=False,
471 ),
472 ]
475class SupplierPriceBreakList(
476 DataExportViewMixin,
477 SupplierPriceBreakMixin,
478 SerializerContextMixin,
479 OutputOptionsMixin,
480 ListCreateAPI,
481):
482 """API endpoint for list view of SupplierPriceBreak object.
484 - GET: Retrieve list of SupplierPriceBreak objects
485 - POST: Create a new SupplierPriceBreak object
486 """
488 output_options = SupplierPriceBreakOutputOptions
490 filterset_class = SupplierPriceBreakFilter
491 filter_backends = SEARCH_ORDER_FILTER
492 ordering_fields = ['quantity', 'supplier', 'SKU', 'price']
494 search_fields = ['part__SKU', 'part__supplier__name']
496 ordering_field_aliases = {'supplier': 'part__supplier__name', 'SKU': 'part__SKU'}
498 ordering = 'quantity'
501class SupplierPriceBreakDetail(SupplierPriceBreakMixin, RetrieveUpdateDestroyAPI):
502 """Detail endpoint for SupplierPriceBreak object."""
505manufacturer_part_api_urls = [
506 path(
507 '<int:pk>/',
508 include([
509 meta_path(ManufacturerPart),
510 path(
511 '',
512 ManufacturerPartDetail.as_view(),
513 name='api-manufacturer-part-detail',
514 ),
515 ]),
516 ),
517 # Catch anything else
518 path('', ManufacturerPartList.as_view(), name='api-manufacturer-part-list'),
519]
522supplier_part_api_urls = [
523 path(
524 '<int:pk>/',
525 include([
526 meta_path(SupplierPart),
527 path('', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
528 ]),
529 ),
530 # Catch anything else
531 path('', SupplierPartList.as_view(), name='api-supplier-part-list'),
532]
535company_api_urls = [
536 path('part/manufacturer/', include(manufacturer_part_api_urls)),
537 path('part/', include(supplier_part_api_urls)),
538 # Supplier price breaks
539 path(
540 'price-break/',
541 include([
542 path(
543 '<int:pk>/',
544 SupplierPriceBreakDetail.as_view(),
545 name='api-part-supplier-price-detail',
546 ),
547 path(
548 '',
549 SupplierPriceBreakList.as_view(),
550 name='api-part-supplier-price-list',
551 ),
552 ]),
553 ),
554 path(
555 '<int:pk>/',
556 include([
557 meta_path(Company),
558 path('', CompanyDetail.as_view(), name='api-company-detail'),
559 ]),
560 ),
561 path(
562 'contact/',
563 include([
564 path(
565 '<int:pk>/',
566 include([
567 meta_path(Contact),
568 path('', ContactDetail.as_view(), name='api-contact-detail'),
569 ]),
570 ),
571 path('', ContactList.as_view(), name='api-contact-list'),
572 ]),
573 ),
574 path(
575 'address/',
576 include([
577 path('<int:pk>/', AddressDetail.as_view(), name='api-address-detail'),
578 path('', AddressList.as_view(), name='api-address-list'),
579 ]),
580 ),
581 path('', CompanyList.as_view(), name='api-company-list'),
582]