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

1"""Provides a JSON API for the Company app.""" 

2 

3from django.db.models import Q 

4from django.urls import include, path 

5from django.utils.translation import gettext_lazy as _ 

6 

7import django_filters.rest_framework.filters as rest_filters 

8from django_filters.rest_framework.filterset import FilterSet 

9 

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) 

22 

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) 

39 

40 

41class CompanyFilter(FilterSet): 

42 """Custom API filters for the CompanyList endpoint.""" 

43 

44 class Meta: 

45 """Metaclass options.""" 

46 

47 model = Company 

48 fields = ['is_customer', 'is_manufacturer', 'is_supplier', 'name', 'active'] 

49 

50 tags = common.filters.TagsFilter() 

51 

52 

53class CompanyMixin(OutputOptionsMixin): 

54 """Mixin class for Company API endpoints.""" 

55 

56 queryset = Company.objects.all() 

57 serializer_class = CompanySerializer 

58 

59 def get_queryset(self): 

60 """Return annotated queryset for the company endpoints.""" 

61 queryset = super().get_queryset() 

62 queryset = CompanySerializer.annotate_queryset(queryset) 

63 

64 return queryset 

65 

66 

67class CompanyList(CompanyMixin, ParameterListMixin, DataExportViewMixin, ListCreateAPI): 

68 """API endpoint for accessing a list of Company objects. 

69 

70 Provides two methods: 

71 

72 - GET: Return list of objects 

73 - POST: Create a new Company object 

74 """ 

75 

76 filter_backends = SEARCH_ORDER_FILTER 

77 

78 filterset_class = CompanyFilter 

79 

80 search_fields = ['name', 'description', 'website', 'tax_id'] 

81 

82 ordering_fields = ['active', 'name', 'parts_supplied', 'parts_manufactured'] 

83 

84 ordering = 'name' 

85 

86 

87class CompanyDetail(CompanyMixin, RetrieveUpdateDestroyAPI): 

88 """API endpoint for detail of a single Company object.""" 

89 

90 

91class ContactList(DataExportViewMixin, ListCreateDestroyAPIView): 

92 """API endpoint for list view of Company model.""" 

93 

94 queryset = Contact.objects.all() 

95 serializer_class = ContactSerializer 

96 

97 filter_backends = SEARCH_ORDER_FILTER 

98 

99 filterset_fields = ['company'] 

100 

101 search_fields = ['company__name', 'name'] 

102 

103 ordering_fields = ['name'] 

104 

105 ordering = 'name' 

106 

107 

108class ContactDetail(RetrieveUpdateDestroyAPI): 

109 """Detail endpoint for Company model.""" 

110 

111 queryset = Contact.objects.all() 

112 serializer_class = ContactSerializer 

113 

114 

115class AddressList(DataExportViewMixin, ListCreateDestroyAPIView): 

116 """API endpoint for list view of Address model.""" 

117 

118 queryset = Address.objects.all() 

119 serializer_class = AddressSerializer 

120 

121 filter_backends = SEARCH_ORDER_FILTER 

122 

123 filterset_fields = ['company'] 

124 

125 ordering_fields = ['title'] 

126 

127 ordering = 'title' 

128 

129 

130class AddressDetail(RetrieveUpdateDestroyAPI): 

131 """API endpoint for a single Address object.""" 

132 

133 queryset = Address.objects.all() 

134 serializer_class = AddressSerializer 

135 

136 

137class ManufacturerPartFilter(FilterSet): 

138 """Custom API filters for the ManufacturerPart list endpoint.""" 

139 

140 class Meta: 

141 """Metaclass options.""" 

142 

143 model = ManufacturerPart 

144 fields = ['manufacturer', 'MPN', 'part', 'tags__name', 'tags__slug'] 

145 

146 # Filter by 'active' status of linked part 

147 part_active = rest_filters.BooleanFilter( 

148 field_name='part__active', label=_('Part is Active') 

149 ) 

150 

151 manufacturer_active = rest_filters.BooleanFilter( 

152 field_name='manufacturer__active', label=_('Manufacturer is Active') 

153 ) 

154 

155 tags = common.filters.TagsFilter(label=_('Tags')) 

156 

157 

158class ManufacturerOutputOptions(OutputConfiguration): 

159 """Available output options for the ManufacturerPart endpoints.""" 

160 

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 ] 

178 

179 

180class ManufacturerPartMixin(SerializerContextMixin): 

181 """Mixin class for ManufacturerPart API endpoints.""" 

182 

183 queryset = ManufacturerPart.objects.all() 

184 serializer_class = ManufacturerPartSerializer 

185 

186 def get_queryset(self, *args, **kwargs): 

187 """Return annotated queryset for the ManufacturerPart list endpoint.""" 

188 queryset = super().get_queryset(*args, **kwargs) 

189 

190 queryset = queryset.prefetch_related('supplier_parts') 

191 queryset = queryset.prefetch_related('part', 'part__pricing_data') 

192 

193 return queryset 

194 

195 

196class ManufacturerPartList( 

197 DataExportViewMixin, 

198 ManufacturerPartMixin, 

199 SerializerContextMixin, 

200 OutputOptionsMixin, 

201 ParameterListMixin, 

202 ListCreateDestroyAPIView, 

203): 

204 """API endpoint for list view of ManufacturerPart object. 

205 

206 - GET: Return list of ManufacturerPart objects 

207 - POST: Create a new ManufacturerPart object 

208 """ 

209 

210 filterset_class = ManufacturerPartFilter 

211 filter_backends = SEARCH_ORDER_FILTER 

212 output_options = ManufacturerOutputOptions 

213 

214 ordering_fields = ['part', 'IPN', 'MPN', 'manufacturer'] 

215 

216 ordering_field_aliases = { 

217 'part': 'part__name', 

218 'IPN': 'part__IPN', 

219 'manufacturer': 'manufacturer__name', 

220 } 

221 

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 ] 

232 

233 

234class ManufacturerPartDetail( 

235 ManufacturerPartMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI 

236): 

237 """API endpoint for detail view of ManufacturerPart object. 

238 

239 - GET: Retrieve detail view 

240 - PATCH: Update object 

241 - DELETE: Delete object 

242 """ 

243 

244 

245class SupplierPartFilter(FilterSet): 

246 """API filters for the SupplierPartList endpoint.""" 

247 

248 class Meta: 

249 """Metaclass option.""" 

250 

251 model = SupplierPart 

252 fields = [ 

253 'supplier', 

254 'part', 

255 'manufacturer_part', 

256 'SKU', 

257 'tags__name', 

258 'tags__slug', 

259 ] 

260 

261 active = rest_filters.BooleanFilter(label=_('Supplier Part is Active')) 

262 

263 primary = rest_filters.BooleanFilter(label=_('Primary Supplier Part')) 

264 

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 ) 

269 

270 # Filter by 'active' status of linked supplier 

271 supplier_active = rest_filters.BooleanFilter( 

272 field_name='supplier__active', label=_('Supplier is Active') 

273 ) 

274 

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 ) 

281 

282 # Filter by 'manufacturer' 

283 manufacturer = rest_filters.ModelChoiceFilter( 

284 label=_('Manufacturer'), 

285 queryset=Company.objects.all(), 

286 field_name='manufacturer_part__manufacturer', 

287 ) 

288 

289 # Filter by 'company' (either manufacturer or supplier) 

290 company = rest_filters.ModelChoiceFilter( 

291 label=_('Company'), queryset=Company.objects.all(), method='filter_company' 

292 ) 

293 

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() 

299 

300 has_stock = rest_filters.BooleanFilter( 

301 label=_('Has Stock'), method='filter_has_stock' 

302 ) 

303 

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) 

310 

311 tags = common.filters.TagsFilter(label=_('Tags')) 

312 

313 

314class SupplierPartOutputOptions(OutputConfiguration): 

315 """Available output options for the SupplierPart endpoints.""" 

316 

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 ] 

344 

345 

346class SupplierPartMixin: 

347 """Mixin class for SupplierPart API endpoints.""" 

348 

349 queryset = SupplierPart.objects.all() 

350 serializer_class = SupplierPartSerializer 

351 

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) 

356 

357 queryset = queryset.prefetch_related('part', 'part__pricing_data') 

358 

359 return queryset 

360 

361 

362class SupplierPartList( 

363 DataExportViewMixin, 

364 SupplierPartMixin, 

365 ParameterListMixin, 

366 OutputOptionsMixin, 

367 ListCreateDestroyAPIView, 

368): 

369 """API endpoint for list view of SupplierPart object. 

370 

371 - GET: Return list of SupplierPart objects 

372 - POST: Create a new SupplierPart object 

373 """ 

374 

375 filterset_class = SupplierPartFilter 

376 filter_backends = SEARCH_ORDER_FILTER 

377 output_options = SupplierPartOutputOptions 

378 

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 ] 

393 

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 } 

402 

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 ] 

416 

417 

418class SupplierPartDetail( 

419 SupplierPartMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI 

420): 

421 """API endpoint for detail view of SupplierPart object. 

422 

423 - GET: Retrieve detail view 

424 - PATCH: Update object 

425 - DELETE: Delete object 

426 """ 

427 

428 output_options = SupplierPartOutputOptions 

429 

430 

431class SupplierPriceBreakFilter(FilterSet): 

432 """Custom API filters for the SupplierPriceBreak list endpoint.""" 

433 

434 class Meta: 

435 """Metaclass options.""" 

436 

437 model = SupplierPriceBreak 

438 fields = ['part', 'quantity'] 

439 

440 base_part = rest_filters.ModelChoiceFilter( 

441 label='Base Part', 

442 queryset=part.models.Part.objects.all(), 

443 field_name='part__part', 

444 ) 

445 

446 supplier = rest_filters.ModelChoiceFilter( 

447 label='Supplier', queryset=Company.objects.all(), field_name='part__supplier' 

448 ) 

449 

450 

451class SupplierPriceBreakMixin: 

452 """Mixin class for SupplierPriceBreak API endpoints.""" 

453 

454 queryset = SupplierPriceBreak.objects.all() 

455 serializer_class = SupplierPriceBreakSerializer 

456 

457 

458class SupplierPriceBreakOutputOptions(OutputConfiguration): 

459 """Available output options for the SupplierPriceBreak endpoints.""" 

460 

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 ] 

473 

474 

475class SupplierPriceBreakList( 

476 DataExportViewMixin, 

477 SupplierPriceBreakMixin, 

478 SerializerContextMixin, 

479 OutputOptionsMixin, 

480 ListCreateAPI, 

481): 

482 """API endpoint for list view of SupplierPriceBreak object. 

483 

484 - GET: Retrieve list of SupplierPriceBreak objects 

485 - POST: Create a new SupplierPriceBreak object 

486 """ 

487 

488 output_options = SupplierPriceBreakOutputOptions 

489 

490 filterset_class = SupplierPriceBreakFilter 

491 filter_backends = SEARCH_ORDER_FILTER 

492 ordering_fields = ['quantity', 'supplier', 'SKU', 'price'] 

493 

494 search_fields = ['part__SKU', 'part__supplier__name'] 

495 

496 ordering_field_aliases = {'supplier': 'part__supplier__name', 'SKU': 'part__SKU'} 

497 

498 ordering = 'quantity' 

499 

500 

501class SupplierPriceBreakDetail(SupplierPriceBreakMixin, RetrieveUpdateDestroyAPI): 

502 """Detail endpoint for SupplierPriceBreak object.""" 

503 

504 

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] 

520 

521 

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] 

533 

534 

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]