572 lines
16 KiB
Python
572 lines
16 KiB
Python
"""Provides a JSON API for the Company app."""
|
|
|
|
from django.db.models import Q
|
|
from django.urls import include, path
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
import django_filters.rest_framework.filters as rest_filters
|
|
from django_filters.rest_framework.filterset import FilterSet
|
|
|
|
import part.models
|
|
from data_exporter.mixins import DataExportViewMixin
|
|
from InvenTree.api import ListCreateDestroyAPIView, ParameterListMixin, meta_path
|
|
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
|
|
from InvenTree.filters import SEARCH_ORDER_FILTER
|
|
from InvenTree.mixins import (
|
|
ListCreateAPI,
|
|
OutputOptionsMixin,
|
|
RetrieveUpdateDestroyAPI,
|
|
SerializerContextMixin,
|
|
)
|
|
|
|
from .models import (
|
|
Address,
|
|
Company,
|
|
Contact,
|
|
ManufacturerPart,
|
|
SupplierPart,
|
|
SupplierPriceBreak,
|
|
)
|
|
from .serializers import (
|
|
AddressSerializer,
|
|
CompanySerializer,
|
|
ContactSerializer,
|
|
ManufacturerPartSerializer,
|
|
SupplierPartSerializer,
|
|
SupplierPriceBreakSerializer,
|
|
)
|
|
|
|
|
|
class CompanyMixin(OutputOptionsMixin):
|
|
"""Mixin class for Company API endpoints."""
|
|
|
|
queryset = Company.objects.all()
|
|
serializer_class = CompanySerializer
|
|
|
|
def get_queryset(self):
|
|
"""Return annotated queryset for the company endpoints."""
|
|
queryset = super().get_queryset()
|
|
queryset = CompanySerializer.annotate_queryset(queryset)
|
|
|
|
return queryset
|
|
|
|
|
|
class CompanyList(CompanyMixin, ParameterListMixin, DataExportViewMixin, ListCreateAPI):
|
|
"""API endpoint for accessing a list of Company objects.
|
|
|
|
Provides two methods:
|
|
|
|
- GET: Return list of objects
|
|
- POST: Create a new Company object
|
|
"""
|
|
|
|
filter_backends = SEARCH_ORDER_FILTER
|
|
|
|
filterset_fields = [
|
|
'is_customer',
|
|
'is_manufacturer',
|
|
'is_supplier',
|
|
'name',
|
|
'active',
|
|
]
|
|
|
|
search_fields = ['name', 'description', 'website', 'tax_id']
|
|
|
|
ordering_fields = ['active', 'name', 'parts_supplied', 'parts_manufactured']
|
|
|
|
ordering = 'name'
|
|
|
|
|
|
class CompanyDetail(CompanyMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for detail of a single Company object."""
|
|
|
|
|
|
class ContactList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|
"""API endpoint for list view of Company model."""
|
|
|
|
queryset = Contact.objects.all()
|
|
serializer_class = ContactSerializer
|
|
|
|
filter_backends = SEARCH_ORDER_FILTER
|
|
|
|
filterset_fields = ['company']
|
|
|
|
search_fields = ['company__name', 'name']
|
|
|
|
ordering_fields = ['name']
|
|
|
|
ordering = 'name'
|
|
|
|
|
|
class ContactDetail(RetrieveUpdateDestroyAPI):
|
|
"""Detail endpoint for Company model."""
|
|
|
|
queryset = Contact.objects.all()
|
|
serializer_class = ContactSerializer
|
|
|
|
|
|
class AddressList(DataExportViewMixin, ListCreateDestroyAPIView):
|
|
"""API endpoint for list view of Address model."""
|
|
|
|
queryset = Address.objects.all()
|
|
serializer_class = AddressSerializer
|
|
|
|
filter_backends = SEARCH_ORDER_FILTER
|
|
|
|
filterset_fields = ['company']
|
|
|
|
ordering_fields = ['title']
|
|
|
|
ordering = 'title'
|
|
|
|
|
|
class AddressDetail(RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single Address object."""
|
|
|
|
queryset = Address.objects.all()
|
|
serializer_class = AddressSerializer
|
|
|
|
|
|
class ManufacturerPartFilter(FilterSet):
|
|
"""Custom API filters for the ManufacturerPart list endpoint."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = ManufacturerPart
|
|
fields = ['manufacturer', 'MPN', 'part', 'tags__name', 'tags__slug']
|
|
|
|
# Filter by 'active' status of linked part
|
|
part_active = rest_filters.BooleanFilter(
|
|
field_name='part__active', label=_('Part is Active')
|
|
)
|
|
|
|
manufacturer_active = rest_filters.BooleanFilter(
|
|
field_name='manufacturer__active', label=_('Manufacturer is Active')
|
|
)
|
|
|
|
|
|
class ManufacturerOutputOptions(OutputConfiguration):
|
|
"""Available output options for the ManufacturerPart endpoints."""
|
|
|
|
OPTIONS = [
|
|
InvenTreeOutputOption(
|
|
description='Include detailed information about the linked Part in the response',
|
|
flag='part_detail',
|
|
default=False,
|
|
),
|
|
InvenTreeOutputOption(
|
|
description='Include detailed information about the Manufacturer in the response',
|
|
flag='manufacturer_detail',
|
|
default=False,
|
|
),
|
|
InvenTreeOutputOption(
|
|
description='Format the output with a more readable (pretty) name',
|
|
flag='pretty',
|
|
default=False,
|
|
),
|
|
]
|
|
|
|
|
|
class ManufacturerPartMixin(SerializerContextMixin):
|
|
"""Mixin class for ManufacturerPart API endpoints."""
|
|
|
|
queryset = ManufacturerPart.objects.all()
|
|
serializer_class = ManufacturerPartSerializer
|
|
|
|
def get_queryset(self, *args, **kwargs):
|
|
"""Return annotated queryset for the ManufacturerPart list endpoint."""
|
|
queryset = super().get_queryset(*args, **kwargs)
|
|
|
|
queryset = queryset.prefetch_related('supplier_parts')
|
|
queryset = queryset.prefetch_related('part', 'part__pricing_data')
|
|
|
|
return queryset
|
|
|
|
|
|
class ManufacturerPartList(
|
|
DataExportViewMixin,
|
|
ManufacturerPartMixin,
|
|
SerializerContextMixin,
|
|
OutputOptionsMixin,
|
|
ParameterListMixin,
|
|
ListCreateDestroyAPIView,
|
|
):
|
|
"""API endpoint for list view of ManufacturerPart object.
|
|
|
|
- GET: Return list of ManufacturerPart objects
|
|
- POST: Create a new ManufacturerPart object
|
|
"""
|
|
|
|
filterset_class = ManufacturerPartFilter
|
|
filter_backends = SEARCH_ORDER_FILTER
|
|
output_options = ManufacturerOutputOptions
|
|
|
|
ordering_fields = ['part', 'IPN', 'MPN', 'manufacturer']
|
|
|
|
ordering_field_aliases = {
|
|
'part': 'part__name',
|
|
'IPN': 'part__IPN',
|
|
'manufacturer': 'manufacturer__name',
|
|
}
|
|
|
|
search_fields = [
|
|
'manufacturer__name',
|
|
'description',
|
|
'MPN',
|
|
'part__IPN',
|
|
'part__name',
|
|
'part__description',
|
|
'tags__name',
|
|
'tags__slug',
|
|
]
|
|
|
|
|
|
class ManufacturerPartDetail(
|
|
ManufacturerPartMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
|
|
):
|
|
"""API endpoint for detail view of ManufacturerPart object.
|
|
|
|
- GET: Retrieve detail view
|
|
- PATCH: Update object
|
|
- DELETE: Delete object
|
|
"""
|
|
|
|
|
|
class SupplierPartFilter(FilterSet):
|
|
"""API filters for the SupplierPartList endpoint."""
|
|
|
|
class Meta:
|
|
"""Metaclass option."""
|
|
|
|
model = SupplierPart
|
|
fields = [
|
|
'supplier',
|
|
'part',
|
|
'manufacturer_part',
|
|
'SKU',
|
|
'tags__name',
|
|
'tags__slug',
|
|
]
|
|
|
|
active = rest_filters.BooleanFilter(label=_('Supplier Part is Active'))
|
|
|
|
primary = rest_filters.BooleanFilter(label=_('Primary Supplier Part'))
|
|
|
|
# Filter by 'active' status of linked part
|
|
part_active = rest_filters.BooleanFilter(
|
|
field_name='part__active', label=_('Internal Part is Active')
|
|
)
|
|
|
|
# Filter by 'active' status of linked supplier
|
|
supplier_active = rest_filters.BooleanFilter(
|
|
field_name='supplier__active', label=_('Supplier is Active')
|
|
)
|
|
|
|
# Filter by the 'MPN' of linked manufacturer part
|
|
MPN = rest_filters.CharFilter(
|
|
label='Manufacturer Part Number',
|
|
field_name='manufacturer_part__MPN',
|
|
lookup_expr='iexact',
|
|
)
|
|
|
|
# Filter by 'manufacturer'
|
|
manufacturer = rest_filters.ModelChoiceFilter(
|
|
label=_('Manufacturer'),
|
|
queryset=Company.objects.all(),
|
|
field_name='manufacturer_part__manufacturer',
|
|
)
|
|
|
|
# Filter by 'company' (either manufacturer or supplier)
|
|
company = rest_filters.ModelChoiceFilter(
|
|
label=_('Company'), queryset=Company.objects.all(), method='filter_company'
|
|
)
|
|
|
|
def filter_company(self, queryset, name, value: int):
|
|
"""Filter the queryset by either manufacturer or supplier."""
|
|
return queryset.filter(
|
|
Q(manufacturer_part__manufacturer=value) | Q(supplier=value)
|
|
).distinct()
|
|
|
|
has_stock = rest_filters.BooleanFilter(
|
|
label=_('Has Stock'), method='filter_has_stock'
|
|
)
|
|
|
|
def filter_has_stock(self, queryset, name, value):
|
|
"""Filter the queryset based on whether the SupplierPart has stock available."""
|
|
if value:
|
|
return queryset.filter(in_stock__gt=0)
|
|
else:
|
|
return queryset.exclude(in_stock__gt=0)
|
|
|
|
|
|
class SupplierPartOutputOptions(OutputConfiguration):
|
|
"""Available output options for the SupplierPart endpoints."""
|
|
|
|
OPTIONS = [
|
|
InvenTreeOutputOption(
|
|
description='Include detailed information about the linked Part in the response',
|
|
flag='part_detail',
|
|
default=False,
|
|
),
|
|
InvenTreeOutputOption(
|
|
description='Include detailed information about the Supplier in the response',
|
|
flag='supplier_detail',
|
|
default=False,
|
|
),
|
|
InvenTreeOutputOption(
|
|
description='Include detailed information about the Manufacturer in the response',
|
|
flag='manufacturer_detail',
|
|
default=False,
|
|
),
|
|
InvenTreeOutputOption(
|
|
flag='manufacturer_part_detail',
|
|
description='Include detailed information about the linked ManufacturerPart in the response',
|
|
default=False,
|
|
),
|
|
InvenTreeOutputOption(
|
|
description='Format the output with a more readable (pretty) name',
|
|
flag='pretty',
|
|
default=False,
|
|
),
|
|
]
|
|
|
|
|
|
class SupplierPartMixin:
|
|
"""Mixin class for SupplierPart API endpoints."""
|
|
|
|
queryset = SupplierPart.objects.all()
|
|
serializer_class = SupplierPartSerializer
|
|
|
|
def get_queryset(self, *args, **kwargs):
|
|
"""Return annotated queryset object for the SupplierPart list."""
|
|
queryset = super().get_queryset(*args, **kwargs)
|
|
queryset = SupplierPartSerializer.annotate_queryset(queryset)
|
|
|
|
queryset = queryset.prefetch_related('part', 'part__pricing_data')
|
|
|
|
return queryset
|
|
|
|
|
|
class SupplierPartList(
|
|
DataExportViewMixin,
|
|
SupplierPartMixin,
|
|
ParameterListMixin,
|
|
OutputOptionsMixin,
|
|
ListCreateDestroyAPIView,
|
|
):
|
|
"""API endpoint for list view of SupplierPart object.
|
|
|
|
- GET: Return list of SupplierPart objects
|
|
- POST: Create a new SupplierPart object
|
|
"""
|
|
|
|
filterset_class = SupplierPartFilter
|
|
filter_backends = SEARCH_ORDER_FILTER
|
|
output_options = SupplierPartOutputOptions
|
|
|
|
ordering_fields = [
|
|
'part',
|
|
'supplier',
|
|
'manufacturer',
|
|
'active',
|
|
'primary',
|
|
'IPN',
|
|
'MPN',
|
|
'SKU',
|
|
'packaging',
|
|
'pack_quantity',
|
|
'in_stock',
|
|
'updated',
|
|
]
|
|
|
|
ordering_field_aliases = {
|
|
'part': 'part__name',
|
|
'supplier': 'supplier__name',
|
|
'manufacturer': 'manufacturer_part__manufacturer__name',
|
|
'pack_quantity': ['pack_quantity_native', 'pack_quantity'],
|
|
'IPN': 'part__IPN',
|
|
'MPN': 'manufacturer_part__MPN',
|
|
}
|
|
|
|
search_fields = [
|
|
'SKU',
|
|
'supplier__name',
|
|
'manufacturer_part__manufacturer__name',
|
|
'description',
|
|
'manufacturer_part__MPN',
|
|
'part__IPN',
|
|
'part__name',
|
|
'part__description',
|
|
'part__keywords',
|
|
'tags__name',
|
|
'tags__slug',
|
|
]
|
|
|
|
|
|
class SupplierPartDetail(
|
|
SupplierPartMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI
|
|
):
|
|
"""API endpoint for detail view of SupplierPart object.
|
|
|
|
- GET: Retrieve detail view
|
|
- PATCH: Update object
|
|
- DELETE: Delete object
|
|
"""
|
|
|
|
output_options = SupplierPartOutputOptions
|
|
|
|
|
|
class SupplierPriceBreakFilter(FilterSet):
|
|
"""Custom API filters for the SupplierPriceBreak list endpoint."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = SupplierPriceBreak
|
|
fields = ['part', 'quantity']
|
|
|
|
base_part = rest_filters.ModelChoiceFilter(
|
|
label='Base Part',
|
|
queryset=part.models.Part.objects.all(),
|
|
field_name='part__part',
|
|
)
|
|
|
|
supplier = rest_filters.ModelChoiceFilter(
|
|
label='Supplier', queryset=Company.objects.all(), field_name='part__supplier'
|
|
)
|
|
|
|
|
|
class SupplierPriceBreakMixin:
|
|
"""Mixin class for SupplierPriceBreak API endpoints."""
|
|
|
|
queryset = SupplierPriceBreak.objects.all()
|
|
serializer_class = SupplierPriceBreakSerializer
|
|
|
|
|
|
class SupplierPriceBreakOutputOptions(OutputConfiguration):
|
|
"""Available output options for the SupplierPriceBreak endpoints."""
|
|
|
|
OPTIONS = [
|
|
InvenTreeOutputOption(
|
|
description='Include detailed information about the linked Part in the response',
|
|
flag='part_detail',
|
|
default=False,
|
|
),
|
|
InvenTreeOutputOption(
|
|
description='Include detailed information about the Supplier in the response',
|
|
flag='supplier_detail',
|
|
default=False,
|
|
),
|
|
]
|
|
|
|
|
|
class SupplierPriceBreakList(
|
|
DataExportViewMixin,
|
|
SupplierPriceBreakMixin,
|
|
SerializerContextMixin,
|
|
OutputOptionsMixin,
|
|
ListCreateAPI,
|
|
):
|
|
"""API endpoint for list view of SupplierPriceBreak object.
|
|
|
|
- GET: Retrieve list of SupplierPriceBreak objects
|
|
- POST: Create a new SupplierPriceBreak object
|
|
"""
|
|
|
|
output_options = SupplierPriceBreakOutputOptions
|
|
|
|
filterset_class = SupplierPriceBreakFilter
|
|
filter_backends = SEARCH_ORDER_FILTER
|
|
ordering_fields = ['quantity', 'supplier', 'SKU', 'price']
|
|
|
|
search_fields = ['part__SKU', 'part__supplier__name']
|
|
|
|
ordering_field_aliases = {'supplier': 'part__supplier__name', 'SKU': 'part__SKU'}
|
|
|
|
ordering = 'quantity'
|
|
|
|
|
|
class SupplierPriceBreakDetail(SupplierPriceBreakMixin, RetrieveUpdateDestroyAPI):
|
|
"""Detail endpoint for SupplierPriceBreak object."""
|
|
|
|
|
|
manufacturer_part_api_urls = [
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
meta_path(ManufacturerPart),
|
|
path(
|
|
'',
|
|
ManufacturerPartDetail.as_view(),
|
|
name='api-manufacturer-part-detail',
|
|
),
|
|
]),
|
|
),
|
|
# Catch anything else
|
|
path('', ManufacturerPartList.as_view(), name='api-manufacturer-part-list'),
|
|
]
|
|
|
|
|
|
supplier_part_api_urls = [
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
meta_path(SupplierPart),
|
|
path('', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
|
|
]),
|
|
),
|
|
# Catch anything else
|
|
path('', SupplierPartList.as_view(), name='api-supplier-part-list'),
|
|
]
|
|
|
|
|
|
company_api_urls = [
|
|
path('part/manufacturer/', include(manufacturer_part_api_urls)),
|
|
path('part/', include(supplier_part_api_urls)),
|
|
# Supplier price breaks
|
|
path(
|
|
'price-break/',
|
|
include([
|
|
path(
|
|
'<int:pk>/',
|
|
SupplierPriceBreakDetail.as_view(),
|
|
name='api-part-supplier-price-detail',
|
|
),
|
|
path(
|
|
'',
|
|
SupplierPriceBreakList.as_view(),
|
|
name='api-part-supplier-price-list',
|
|
),
|
|
]),
|
|
),
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
meta_path(Company),
|
|
path('', CompanyDetail.as_view(), name='api-company-detail'),
|
|
]),
|
|
),
|
|
path(
|
|
'contact/',
|
|
include([
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
meta_path(Contact),
|
|
path('', ContactDetail.as_view(), name='api-contact-detail'),
|
|
]),
|
|
),
|
|
path('', ContactList.as_view(), name='api-contact-list'),
|
|
]),
|
|
),
|
|
path(
|
|
'address/',
|
|
include([
|
|
path('<int:pk>/', AddressDetail.as_view(), name='api-address-detail'),
|
|
path('', AddressList.as_view(), name='api-address-list'),
|
|
]),
|
|
),
|
|
path('', CompanyList.as_view(), name='api-company-list'),
|
|
]
|