Coverage for src/backend/InvenTree/company/serializers.py: 84%
143 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"""JSON serializers for Company app."""
3from django.db.models import Prefetch
4from django.utils.translation import gettext_lazy as _
6from rest_framework import serializers
7from sql_util.utils import SubqueryCount
9import common.filters
10import company.filters
11import part.filters
12import part.serializers as part_serializers
13from importer.registry import register_importer
14from InvenTree.mixins import DataImportExportSerializerMixin
15from InvenTree.ready import isGeneratingSchema
16from InvenTree.serializers import (
17 FilterableSerializerMixin,
18 InvenTreeCurrencySerializer,
19 InvenTreeDecimalField,
20 InvenTreeImageSerializerField,
21 InvenTreeModelSerializer,
22 InvenTreeMoneySerializer,
23 InvenTreeTaggitSerializer,
24 NotesFieldMixin,
25 OptionalField,
26)
28from .models import (
29 Address,
30 Company,
31 Contact,
32 ManufacturerPart,
33 SupplierPart,
34 SupplierPriceBreak,
35)
38class CompanyBriefSerializer(InvenTreeModelSerializer):
39 """Serializer for Company object (limited detail)."""
41 class Meta:
42 """Metaclass options."""
44 model = Company
45 fields = [
46 'pk',
47 'active',
48 'name',
49 'description',
50 'image',
51 'thumbnail',
52 'currency',
53 'tax_id',
54 ]
55 read_only_fields = ['currency']
57 image = InvenTreeImageSerializerField(read_only=True)
59 thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
62@register_importer()
63class AddressSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
64 """Serializer for the Address Model."""
66 class Meta:
67 """Metaclass options."""
69 model = Address
70 fields = [
71 'pk',
72 'company',
73 'title',
74 'primary',
75 'line1',
76 'line2',
77 'postal_code',
78 'postal_city',
79 'province',
80 'country',
81 'shipping_notes',
82 'internal_shipping_notes',
83 'link',
84 ]
87class AddressBriefSerializer(InvenTreeModelSerializer):
88 """Serializer for Address Model (limited)."""
90 class Meta:
91 """Metaclass options."""
93 model = Address
94 fields = [
95 'pk',
96 'line1',
97 'line2',
98 'postal_code',
99 'postal_city',
100 'province',
101 'country',
102 'shipping_notes',
103 'internal_shipping_notes',
104 ]
107@register_importer()
108class CompanySerializer(
109 FilterableSerializerMixin,
110 DataImportExportSerializerMixin,
111 InvenTreeTaggitSerializer,
112 NotesFieldMixin,
113 InvenTreeModelSerializer,
114):
115 """Serializer for Company object (full detail)."""
117 export_exclude_fields = ['primary_address']
119 import_exclude_fields = ['image']
121 class Meta:
122 """Metaclass options."""
124 model = Company
125 fields = [
126 'pk',
127 'name',
128 'description',
129 'website',
130 'name',
131 'phone',
132 'email',
133 'currency',
134 'contact',
135 'link',
136 'image',
137 'active',
138 'is_customer',
139 'is_manufacturer',
140 'is_supplier',
141 'notes',
142 'parts_supplied',
143 'parts_manufactured',
144 'primary_address',
145 'tax_id',
146 'parameters',
147 'tags',
148 ]
150 @staticmethod
151 def annotate_queryset(queryset):
152 """Annotate the supplied queryset with aggregated information."""
153 # Add count of parts manufactured
154 queryset = queryset.annotate(
155 parts_manufactured=SubqueryCount('manufactured_parts')
156 )
158 queryset = queryset.annotate(parts_supplied=SubqueryCount('supplied_parts'))
160 return queryset
162 primary_address = OptionalField(
163 serializer_class=AddressBriefSerializer,
164 serializer_kwargs={'read_only': True, 'many': False, 'allow_null': True},
165 default_include=False,
166 filter_name='address_detail',
167 prefetch_fields=[
168 Prefetch(
169 'addresses',
170 queryset=Address.objects.filter(primary=True),
171 to_attr='primary_address_list',
172 )
173 ],
174 )
176 image = InvenTreeImageSerializerField(required=False, allow_null=True)
178 email = serializers.EmailField(
179 required=False, default='', allow_blank=True, allow_null=True
180 )
182 parts_supplied = serializers.IntegerField(read_only=True)
184 parts_manufactured = serializers.IntegerField(read_only=True)
186 currency = InvenTreeCurrencySerializer(
187 help_text=_('Default currency used for this supplier'), required=True
188 )
190 tags = common.filters.enable_tags_filter()
192 parameters = common.filters.enable_parameters_filter()
195@register_importer()
196class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
197 """Serializer class for the Contact model."""
199 class Meta:
200 """Metaclass options."""
202 model = Contact
203 fields = ['pk', 'company', 'company_name', 'name', 'phone', 'email', 'role']
205 company_name = serializers.CharField(
206 label=_('Company Name'), source='company.name', read_only=True
207 )
210@register_importer()
211class ManufacturerPartSerializer(
212 FilterableSerializerMixin,
213 DataImportExportSerializerMixin,
214 InvenTreeTaggitSerializer,
215 NotesFieldMixin,
216 InvenTreeModelSerializer,
217):
218 """Serializer for ManufacturerPart object."""
220 class Meta:
221 """Metaclass options."""
223 model = ManufacturerPart
224 fields = [
225 'pk',
226 'part',
227 'part_detail',
228 'pretty_name',
229 'manufacturer',
230 'manufacturer_detail',
231 'description',
232 'MPN',
233 'link',
234 'barcode_hash',
235 'notes',
236 'tags',
237 'parameters',
238 ]
240 tags = common.filters.enable_tags_filter()
242 parameters = common.filters.enable_parameters_filter()
244 part_detail = OptionalField(
245 serializer_class=part_serializers.PartBriefSerializer,
246 serializer_kwargs={
247 'source': 'part',
248 'many': False,
249 'read_only': True,
250 'allow_null': True,
251 },
252 default_include=True,
253 prefetch_fields=['part__category'],
254 )
256 pretty_name = OptionalField(
257 serializer_class=serializers.CharField,
258 serializer_kwargs={'read_only': True, 'allow_null': True},
259 filter_name='pretty',
260 )
262 manufacturer = serializers.PrimaryKeyRelatedField(
263 queryset=Company.objects.filter(is_manufacturer=True)
264 )
266 manufacturer_detail = OptionalField(
267 serializer_class=CompanyBriefSerializer,
268 serializer_kwargs={
269 'source': 'manufacturer',
270 'many': False,
271 'read_only': True,
272 'allow_null': True,
273 },
274 default_include=True,
275 prefetch_fields=['manufacturer'],
276 )
279class SupplierPriceBreakBriefSerializer(
280 FilterableSerializerMixin, InvenTreeModelSerializer
281):
282 """Brief serializer for SupplierPriceBreak object.
284 Used to provide a list of price breaks against the SupplierPart object.
285 """
287 no_filters = True
289 class Meta:
290 """Metaclass options."""
292 model = SupplierPriceBreak
293 fields = [
294 'pk',
295 'part',
296 'quantity',
297 'price',
298 'price_currency',
299 'supplier',
300 'updated',
301 ]
303 quantity = InvenTreeDecimalField()
304 price = InvenTreeMoneySerializer(allow_null=True, required=True, label=_('Price'))
305 price_currency = InvenTreeCurrencySerializer()
307 supplier = serializers.PrimaryKeyRelatedField(
308 source='part.supplier', many=False, read_only=True
309 )
312@register_importer()
313class SupplierPartSerializer(
314 FilterableSerializerMixin,
315 DataImportExportSerializerMixin,
316 InvenTreeTaggitSerializer,
317 NotesFieldMixin,
318 InvenTreeModelSerializer,
319):
320 """Serializer for SupplierPart object."""
322 no_filters = True
324 export_exclude_fields = ['tags']
326 export_child_fields = [
327 'part_detail.name',
328 'part_detail.description',
329 'part_detail.IPN',
330 'supplier_detail.name',
331 'manufacturer_detail.name',
332 ]
334 class Meta:
335 """Metaclass options."""
337 model = SupplierPart
338 fields = [
339 'available',
340 'availability_updated',
341 'description',
342 'in_stock',
343 'on_order',
344 'link',
345 'active',
346 'primary',
347 'manufacturer_detail',
348 'manufacturer_part',
349 'manufacturer_part_detail',
350 'MPN',
351 'note',
352 'pk',
353 'barcode_hash',
354 'packaging',
355 'pack_quantity',
356 'pack_quantity_native',
357 'part',
358 'pretty_name',
359 'SKU',
360 'supplier',
361 'supplier_detail',
362 'updated',
363 'notes',
364 'part_detail',
365 'tags',
366 'price_breaks',
367 'parameters',
368 ]
369 read_only_fields = [
370 'availability_updated',
371 'barcode_hash',
372 'pack_quantity_native',
373 ]
375 tags = common.filters.enable_tags_filter()
377 def __init__(self, *args, **kwargs):
378 """Initialize this serializer with extra detail fields as required."""
379 # Check if 'available' quantity was supplied
380 self.has_available_quantity = 'available' in kwargs.get('data', {})
382 brief = kwargs.pop('brief', False)
384 super().__init__(*args, **kwargs)
386 if isGeneratingSchema(): 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true
387 return
389 if brief:
390 self.fields.pop('available', None)
391 self.fields.pop('on_order', None)
392 self.fields.pop('availability_updated', None)
394 # Annotated field showing total in-stock quantity
395 in_stock = serializers.FloatField(
396 read_only=True, allow_null=True, label=_('In Stock')
397 )
399 on_order = serializers.FloatField(
400 read_only=True, allow_null=True, label=_('On Order')
401 )
403 available = serializers.FloatField(required=False, label=_('Available'))
405 pack_quantity_native = serializers.FloatField(read_only=True)
407 price_breaks = OptionalField(
408 serializer_class=SupplierPriceBreakBriefSerializer,
409 serializer_kwargs={
410 'source': 'pricebreaks',
411 'many': True,
412 'read_only': True,
413 'allow_null': True,
414 'label': _('Price Breaks'),
415 },
416 default_include=False,
417 filter_name='price_breaks',
418 prefetch_fields=['pricebreaks'],
419 )
421 parameters = common.filters.enable_parameters_filter()
423 part_detail = OptionalField(
424 serializer_class=part_serializers.PartBriefSerializer,
425 serializer_kwargs={
426 'label': _('Part'),
427 'source': 'part',
428 'many': False,
429 'read_only': True,
430 'allow_null': True,
431 },
432 default_include=False,
433 prefetch_fields=['part', 'part__pricing_data'],
434 )
436 supplier_detail = OptionalField(
437 serializer_class=CompanyBriefSerializer,
438 serializer_kwargs={
439 'label': _('Supplier'),
440 'source': 'supplier',
441 'many': False,
442 'read_only': True,
443 'allow_null': True,
444 },
445 default_include=False,
446 prefetch_fields=['supplier'],
447 )
449 manufacturer_detail = OptionalField(
450 serializer_class=CompanyBriefSerializer,
451 serializer_kwargs={
452 'label': _('Manufacturer'),
453 'source': 'manufacturer_part.manufacturer',
454 'many': False,
455 'read_only': True,
456 'allow_null': True,
457 },
458 default_include=False,
459 prefetch_fields=['manufacturer_part__manufacturer'],
460 )
462 pretty_name = OptionalField(
463 serializer_class=serializers.CharField,
464 serializer_kwargs={
465 'read_only': True,
466 'allow_null': True,
467 'label': _('Pretty Name'),
468 },
469 default_include=False,
470 filter_name='pretty',
471 )
473 supplier = serializers.PrimaryKeyRelatedField(
474 label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
475 )
477 manufacturer_part_detail = OptionalField(
478 serializer_class=ManufacturerPartSerializer,
479 serializer_kwargs={
480 'label': _('Manufacturer Part'),
481 'source': 'manufacturer_part',
482 'part_detail': False,
483 'read_only': True,
484 'allow_null': True,
485 },
486 default_include=False,
487 prefetch_fields=['manufacturer_part'],
488 )
490 MPN = serializers.CharField(
491 source='manufacturer_part.MPN', read_only=True, allow_null=True, label=_('MPN')
492 )
494 # Date fields
495 updated = serializers.DateTimeField(allow_null=True, read_only=True)
497 @staticmethod
498 def annotate_queryset(queryset):
499 """Annotate the SupplierPart queryset with extra fields.
501 Fields:
502 in_stock: Current stock quantity for each SupplierPart
503 """
504 queryset = queryset.annotate(in_stock=part.filters.annotate_total_stock())
506 queryset = queryset.annotate(
507 on_order=company.filters.annotate_on_order_quantity()
508 )
510 queryset = queryset.prefetch_related('supplier', 'manufacturer_part')
512 return queryset
514 def update(self, supplier_part, data):
515 """Custom update functionality for the serializer."""
516 available = data.pop('available', None)
518 response = super().update(supplier_part, data)
520 if available is not None and self.has_available_quantity:
521 supplier_part.update_available_quantity(available)
523 return response
525 def create(self, validated_data):
526 """Extract manufacturer data and process ManufacturerPart."""
527 # Extract 'available' quantity from the serializer
528 available = validated_data.pop('available', None)
530 # Create SupplierPart
531 supplier_part = super().create(validated_data)
533 if available is not None and self.has_available_quantity:
534 supplier_part.update_available_quantity(available)
536 # Get ManufacturerPart raw data (unvalidated)
537 manufacturer = self.initial_data.get('manufacturer', None)
538 MPN = self.initial_data.get('MPN', None)
540 if manufacturer and MPN:
541 kwargs = {'manufacturer': manufacturer, 'MPN': MPN}
542 supplier_part.save(**kwargs)
544 return supplier_part
547@register_importer()
548class SupplierPriceBreakSerializer(
549 DataImportExportSerializerMixin,
550 SupplierPriceBreakBriefSerializer,
551 InvenTreeModelSerializer,
552):
553 """Serializer for SupplierPriceBreak object.
555 Note that this inherits from the SupplierPriceBreakBriefSerializer,
556 and does so to prevent circular serializer import issues.
557 """
559 class Meta:
560 """Metaclass options."""
562 model = SupplierPriceBreak
563 fields = [
564 *SupplierPriceBreakBriefSerializer.Meta.fields,
565 'supplier_detail',
566 'part_detail',
567 ]
569 @staticmethod
570 def annotate_queryset(queryset):
571 """Prefetch related fields for the queryset."""
572 queryset = queryset.select_related('part', 'part__supplier', 'part__part')
574 return queryset
576 supplier_detail = OptionalField(
577 serializer_class=CompanyBriefSerializer,
578 serializer_kwargs={
579 'source': 'part.supplier',
580 'many': False,
581 'read_only': True,
582 'allow_null': True,
583 },
584 default_include=False,
585 prefetch_fields=['part__supplier'],
586 )
588 part_detail = OptionalField(
589 serializer_class=SupplierPartSerializer,
590 serializer_kwargs={
591 'source': 'part',
592 'brief': True,
593 'many': False,
594 'read_only': True,
595 'allow_null': True,
596 },
597 default_include=False,
598 prefetch_fields=['part', 'part__part', 'part__part__pricing_data'],
599 )