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

1"""JSON serializers for Company app.""" 

2 

3from django.db.models import Prefetch 

4from django.utils.translation import gettext_lazy as _ 

5 

6from rest_framework import serializers 

7from sql_util.utils import SubqueryCount 

8 

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) 

27 

28from .models import ( 

29 Address, 

30 Company, 

31 Contact, 

32 ManufacturerPart, 

33 SupplierPart, 

34 SupplierPriceBreak, 

35) 

36 

37 

38class CompanyBriefSerializer(InvenTreeModelSerializer): 

39 """Serializer for Company object (limited detail).""" 

40 

41 class Meta: 

42 """Metaclass options.""" 

43 

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'] 

56 

57 image = InvenTreeImageSerializerField(read_only=True) 

58 

59 thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) 

60 

61 

62@register_importer() 

63class AddressSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): 

64 """Serializer for the Address Model.""" 

65 

66 class Meta: 

67 """Metaclass options.""" 

68 

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 ] 

85 

86 

87class AddressBriefSerializer(InvenTreeModelSerializer): 

88 """Serializer for Address Model (limited).""" 

89 

90 class Meta: 

91 """Metaclass options.""" 

92 

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 ] 

105 

106 

107@register_importer() 

108class CompanySerializer( 

109 FilterableSerializerMixin, 

110 DataImportExportSerializerMixin, 

111 InvenTreeTaggitSerializer, 

112 NotesFieldMixin, 

113 InvenTreeModelSerializer, 

114): 

115 """Serializer for Company object (full detail).""" 

116 

117 export_exclude_fields = ['primary_address'] 

118 

119 import_exclude_fields = ['image'] 

120 

121 class Meta: 

122 """Metaclass options.""" 

123 

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 ] 

149 

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 ) 

157 

158 queryset = queryset.annotate(parts_supplied=SubqueryCount('supplied_parts')) 

159 

160 return queryset 

161 

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 ) 

175 

176 image = InvenTreeImageSerializerField(required=False, allow_null=True) 

177 

178 email = serializers.EmailField( 

179 required=False, default='', allow_blank=True, allow_null=True 

180 ) 

181 

182 parts_supplied = serializers.IntegerField(read_only=True) 

183 

184 parts_manufactured = serializers.IntegerField(read_only=True) 

185 

186 currency = InvenTreeCurrencySerializer( 

187 help_text=_('Default currency used for this supplier'), required=True 

188 ) 

189 

190 tags = common.filters.enable_tags_filter() 

191 

192 parameters = common.filters.enable_parameters_filter() 

193 

194 

195@register_importer() 

196class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): 

197 """Serializer class for the Contact model.""" 

198 

199 class Meta: 

200 """Metaclass options.""" 

201 

202 model = Contact 

203 fields = ['pk', 'company', 'company_name', 'name', 'phone', 'email', 'role'] 

204 

205 company_name = serializers.CharField( 

206 label=_('Company Name'), source='company.name', read_only=True 

207 ) 

208 

209 

210@register_importer() 

211class ManufacturerPartSerializer( 

212 FilterableSerializerMixin, 

213 DataImportExportSerializerMixin, 

214 InvenTreeTaggitSerializer, 

215 NotesFieldMixin, 

216 InvenTreeModelSerializer, 

217): 

218 """Serializer for ManufacturerPart object.""" 

219 

220 class Meta: 

221 """Metaclass options.""" 

222 

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 ] 

239 

240 tags = common.filters.enable_tags_filter() 

241 

242 parameters = common.filters.enable_parameters_filter() 

243 

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 ) 

255 

256 pretty_name = OptionalField( 

257 serializer_class=serializers.CharField, 

258 serializer_kwargs={'read_only': True, 'allow_null': True}, 

259 filter_name='pretty', 

260 ) 

261 

262 manufacturer = serializers.PrimaryKeyRelatedField( 

263 queryset=Company.objects.filter(is_manufacturer=True) 

264 ) 

265 

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 ) 

277 

278 

279class SupplierPriceBreakBriefSerializer( 

280 FilterableSerializerMixin, InvenTreeModelSerializer 

281): 

282 """Brief serializer for SupplierPriceBreak object. 

283 

284 Used to provide a list of price breaks against the SupplierPart object. 

285 """ 

286 

287 no_filters = True 

288 

289 class Meta: 

290 """Metaclass options.""" 

291 

292 model = SupplierPriceBreak 

293 fields = [ 

294 'pk', 

295 'part', 

296 'quantity', 

297 'price', 

298 'price_currency', 

299 'supplier', 

300 'updated', 

301 ] 

302 

303 quantity = InvenTreeDecimalField() 

304 price = InvenTreeMoneySerializer(allow_null=True, required=True, label=_('Price')) 

305 price_currency = InvenTreeCurrencySerializer() 

306 

307 supplier = serializers.PrimaryKeyRelatedField( 

308 source='part.supplier', many=False, read_only=True 

309 ) 

310 

311 

312@register_importer() 

313class SupplierPartSerializer( 

314 FilterableSerializerMixin, 

315 DataImportExportSerializerMixin, 

316 InvenTreeTaggitSerializer, 

317 NotesFieldMixin, 

318 InvenTreeModelSerializer, 

319): 

320 """Serializer for SupplierPart object.""" 

321 

322 no_filters = True 

323 

324 export_exclude_fields = ['tags'] 

325 

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 ] 

333 

334 class Meta: 

335 """Metaclass options.""" 

336 

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 ] 

374 

375 tags = common.filters.enable_tags_filter() 

376 

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', {}) 

381 

382 brief = kwargs.pop('brief', False) 

383 

384 super().__init__(*args, **kwargs) 

385 

386 if isGeneratingSchema(): 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true

387 return 

388 

389 if brief: 

390 self.fields.pop('available', None) 

391 self.fields.pop('on_order', None) 

392 self.fields.pop('availability_updated', None) 

393 

394 # Annotated field showing total in-stock quantity 

395 in_stock = serializers.FloatField( 

396 read_only=True, allow_null=True, label=_('In Stock') 

397 ) 

398 

399 on_order = serializers.FloatField( 

400 read_only=True, allow_null=True, label=_('On Order') 

401 ) 

402 

403 available = serializers.FloatField(required=False, label=_('Available')) 

404 

405 pack_quantity_native = serializers.FloatField(read_only=True) 

406 

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 ) 

420 

421 parameters = common.filters.enable_parameters_filter() 

422 

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 ) 

435 

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 ) 

448 

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 ) 

461 

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 ) 

472 

473 supplier = serializers.PrimaryKeyRelatedField( 

474 label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True) 

475 ) 

476 

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 ) 

489 

490 MPN = serializers.CharField( 

491 source='manufacturer_part.MPN', read_only=True, allow_null=True, label=_('MPN') 

492 ) 

493 

494 # Date fields 

495 updated = serializers.DateTimeField(allow_null=True, read_only=True) 

496 

497 @staticmethod 

498 def annotate_queryset(queryset): 

499 """Annotate the SupplierPart queryset with extra fields. 

500 

501 Fields: 

502 in_stock: Current stock quantity for each SupplierPart 

503 """ 

504 queryset = queryset.annotate(in_stock=part.filters.annotate_total_stock()) 

505 

506 queryset = queryset.annotate( 

507 on_order=company.filters.annotate_on_order_quantity() 

508 ) 

509 

510 queryset = queryset.prefetch_related('supplier', 'manufacturer_part') 

511 

512 return queryset 

513 

514 def update(self, supplier_part, data): 

515 """Custom update functionality for the serializer.""" 

516 available = data.pop('available', None) 

517 

518 response = super().update(supplier_part, data) 

519 

520 if available is not None and self.has_available_quantity: 

521 supplier_part.update_available_quantity(available) 

522 

523 return response 

524 

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) 

529 

530 # Create SupplierPart 

531 supplier_part = super().create(validated_data) 

532 

533 if available is not None and self.has_available_quantity: 

534 supplier_part.update_available_quantity(available) 

535 

536 # Get ManufacturerPart raw data (unvalidated) 

537 manufacturer = self.initial_data.get('manufacturer', None) 

538 MPN = self.initial_data.get('MPN', None) 

539 

540 if manufacturer and MPN: 

541 kwargs = {'manufacturer': manufacturer, 'MPN': MPN} 

542 supplier_part.save(**kwargs) 

543 

544 return supplier_part 

545 

546 

547@register_importer() 

548class SupplierPriceBreakSerializer( 

549 DataImportExportSerializerMixin, 

550 SupplierPriceBreakBriefSerializer, 

551 InvenTreeModelSerializer, 

552): 

553 """Serializer for SupplierPriceBreak object. 

554 

555 Note that this inherits from the SupplierPriceBreakBriefSerializer, 

556 and does so to prevent circular serializer import issues. 

557 """ 

558 

559 class Meta: 

560 """Metaclass options.""" 

561 

562 model = SupplierPriceBreak 

563 fields = [ 

564 *SupplierPriceBreakBriefSerializer.Meta.fields, 

565 'supplier_detail', 

566 'part_detail', 

567 ] 

568 

569 @staticmethod 

570 def annotate_queryset(queryset): 

571 """Prefetch related fields for the queryset.""" 

572 queryset = queryset.select_related('part', 'part__supplier', 'part__part') 

573 

574 return queryset 

575 

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 ) 

587 

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 )