Coverage for src/backend/InvenTree/order/serializers.py: 53%

880 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""JSON serializers for the Order API.""" 

2 

3from decimal import Decimal 

4 

5from django.core.exceptions import ValidationError as DjangoValidationError 

6from django.db import models, transaction 

7from django.db.models import BooleanField, Case, ExpressionWrapper, F, Q, Value, When 

8from django.db.models.functions import Coalesce, Greatest 

9from django.utils.translation import gettext_lazy as _ 

10 

11from rest_framework import serializers 

12from rest_framework.serializers import ValidationError 

13from sql_util.utils import SubqueryCount, SubquerySum 

14 

15import build.serializers 

16import common.filters 

17import company.models as company_models 

18import order.models 

19import part.filters as part_filters 

20import stock.models 

21import stock.serializers 

22from common.settings import get_global_setting 

23from company.serializers import ( 

24 AddressBriefSerializer, 

25 CompanyBriefSerializer, 

26 ContactSerializer, 

27 SupplierPartSerializer, 

28) 

29from generic.states.fields import InvenTreeCustomStatusSerializerMixin 

30from importer.registry import register_importer 

31from InvenTree.helpers import extract_serial_numbers, hash_barcode, normalize, str2bool 

32from InvenTree.mixins import DataImportExportSerializerMixin 

33from InvenTree.serializers import ( 

34 CustomStatusSerializerMixin, 

35 FilterableSerializerMixin, 

36 InvenTreeCurrencySerializer, 

37 InvenTreeDecimalField, 

38 InvenTreeModelSerializer, 

39 InvenTreeMoneySerializer, 

40 InvenTreeTaggitSerializer, 

41 NotesFieldMixin, 

42 OptionalField, 

43) 

44from order.status_codes import ( 

45 PurchaseOrderStatusGroups, 

46 ReturnOrderLineStatus, 

47 ReturnOrderStatus, 

48 SalesOrderStatusGroups, 

49 TransferOrderStatusGroups, 

50) 

51from part.serializers import PartBriefSerializer 

52from stock.status_codes import StockStatus 

53from users.serializers import OwnerSerializer, UserSerializer 

54 

55 

56class TotalPriceMixin(serializers.Serializer): 

57 """Serializer mixin which provides total price fields.""" 

58 

59 total_price = InvenTreeMoneySerializer(allow_null=True, read_only=True) 

60 

61 order_currency = InvenTreeCurrencySerializer( 

62 allow_blank=True, 

63 allow_null=True, 

64 required=False, 

65 label=_('Order Currency'), 

66 help_text=_('Currency for this order (leave blank to use company default)'), 

67 ) 

68 

69 

70class DuplicateOrderSerializer(serializers.Serializer): 

71 """Serializer for specifying options when duplicating an order.""" 

72 

73 class Meta: 

74 """Metaclass options.""" 

75 

76 fields = ['order_id', 'copy_lines', 'copy_extra_lines', 'copy_parameters'] 

77 

78 order_id = serializers.IntegerField( 

79 required=True, label=_('Order ID'), help_text=_('ID of the order to duplicate') 

80 ) 

81 

82 copy_lines = serializers.BooleanField( 

83 required=False, 

84 default=True, 

85 label=_('Copy Lines'), 

86 help_text=_('Copy line items from the original order'), 

87 ) 

88 

89 copy_extra_lines = serializers.BooleanField( 

90 required=False, 

91 default=True, 

92 label=_('Copy Extra Lines'), 

93 help_text=_('Copy extra line items from the original order'), 

94 ) 

95 

96 copy_parameters = serializers.BooleanField( 

97 required=False, 

98 default=True, 

99 label=_('Copy Parameters'), 

100 help_text=_('Copy order parameters from the original order'), 

101 ) 

102 

103 

104class AbstractOrderSerializer( 

105 CustomStatusSerializerMixin, 

106 DataImportExportSerializerMixin, 

107 InvenTreeTaggitSerializer, 

108 FilterableSerializerMixin, 

109 serializers.Serializer, 

110): 

111 """Abstract serializer class which provides fields common to all order types.""" 

112 

113 export_exclude_fields = ['notes', 'duplicate'] 

114 

115 import_exclude_fields = ['notes', 'duplicate'] 

116 

117 # Number of line items in this order 

118 line_items = serializers.IntegerField( 

119 read_only=True, allow_null=True, label=_('Line Items') 

120 ) 

121 

122 # Number of completed line items (this is an annotated field) 

123 completed_lines = serializers.IntegerField( 

124 read_only=True, allow_null=True, label=_('Completed Lines') 

125 ) 

126 

127 # status field cannot be set directly 

128 status = serializers.IntegerField(read_only=True, label=_('Order Status')) 

129 

130 # can be set directly, but must be valid for the current order status 

131 status_custom_key = serializers.IntegerField( 

132 label=_('Custom Status Key'), 

133 help_text=_('Update order status to a custom value for this logical value'), 

134 allow_null=True, 

135 default=None, 

136 ) 

137 

138 # Reference string is *required* 

139 reference = serializers.CharField(required=True) 

140 

141 # Detail for point-of-contact field 

142 contact_detail = OptionalField( 

143 serializer_class=ContactSerializer, 

144 serializer_kwargs={ 

145 'source': 'contact', 

146 'many': False, 

147 'read_only': True, 

148 'allow_null': True, 

149 }, 

150 default_include=True, 

151 prefetch_fields=['contact'], 

152 ) 

153 

154 # Detail for responsible field 

155 responsible_detail = OptionalField( 

156 serializer_class=OwnerSerializer, 

157 serializer_kwargs={ 

158 'source': 'responsible', 

159 'read_only': True, 

160 'allow_null': True, 

161 'many': False, 

162 }, 

163 default_include=True, 

164 prefetch_fields=['responsible'], 

165 ) 

166 

167 project_code_label = common.filters.enable_project_label_filter() 

168 project_code_detail = common.filters.enable_project_code_filter() 

169 

170 # Detail for address field 

171 address_detail = OptionalField( 

172 serializer_class=AddressBriefSerializer, 

173 serializer_kwargs={ 

174 'source': 'address', 

175 'many': False, 

176 'read_only': True, 

177 'allow_null': True, 

178 }, 

179 default_include=True, 

180 prefetch_fields=['address'], 

181 ) 

182 

183 parameters = common.filters.enable_parameters_filter() 

184 

185 tags = common.filters.enable_tags_filter() 

186 

187 # Boolean field indicating if this order is overdue (Note: must be annotated) 

188 overdue = serializers.BooleanField(read_only=True, allow_null=True) 

189 

190 barcode_hash = serializers.CharField(read_only=True) 

191 

192 creation_date = serializers.DateField( 

193 required=False, allow_null=True, label=_('Creation Date') 

194 ) 

195 

196 created_by = UserSerializer(read_only=True) 

197 

198 duplicate = DuplicateOrderSerializer( 

199 label=_('Duplicate Order'), 

200 help_text=_('Specify options for duplicating this order'), 

201 required=False, 

202 write_only=True, 

203 ) 

204 

205 def validate_reference(self, reference): 

206 """Custom validation for the reference field.""" 

207 self.Meta.model.validate_reference_field(reference) 

208 return reference 

209 

210 def validate_status_custom_key(self, value): 

211 """Validate the status_custom_key field. 

212 

213 Ensure the custom status key is valid for the logical order status. 

214 """ 

215 if value is None: 215 ↛ 218line 215 didn't jump to line 218 because the condition on line 215 was always true

216 return value 

217 

218 from generic.states.custom import get_logical_value 

219 

220 if not isinstance(value, int): 

221 raise ValidationError(_('Custom status key must be an integer')) 

222 

223 try: 

224 custom_status = get_logical_value( 

225 value, model=self.Meta.model._meta.model_name 

226 ) 

227 except: 

228 raise ValidationError(_('Invalid custom status key')) 

229 

230 if custom_status.logical_key is not self.instance.status: 

231 raise ValidationError(_('Invalid custom status key for this order status')) 

232 

233 return value 

234 

235 @staticmethod 

236 def annotate_queryset(queryset): 

237 """Add extra information to the queryset.""" 

238 queryset = queryset.annotate(line_items=SubqueryCount('lines')) 

239 queryset = queryset.select_related('created_by') 

240 

241 return queryset 

242 

243 @staticmethod 

244 def order_fields(extra_fields): 

245 """Construct a set of fields for this serializer.""" 

246 return [ 

247 'pk', 

248 'created_by', 

249 'creation_date', 

250 'issue_date', 

251 'start_date', 

252 'target_date', 

253 'description', 

254 'line_items', 

255 'completed_lines', 

256 'link', 

257 'project_code', 

258 'reference', 

259 'responsible', 

260 'contact', 

261 'address', 

262 'status', 

263 'status_text', 

264 'status_custom_key', 

265 'notes', 

266 'barcode_hash', 

267 'overdue', 

268 'duplicate', 

269 # Extra detail fields 

270 'address_detail', 

271 'contact_detail', 

272 'project_code_detail', 

273 'project_code_label', 

274 'responsible_detail', 

275 'parameters', 

276 'tags', 

277 *extra_fields, 

278 ] 

279 

280 def clean_line_item(self, line): 

281 """Clean a line item object (when duplicating).""" 

282 line.pk = None 

283 line.order = self 

284 

285 @transaction.atomic 

286 def create(self, validated_data): 

287 """Create a new order object. 

288 

289 Optionally, copy line items from an existing order. 

290 """ 

291 duplicate = validated_data.pop('duplicate', None) 

292 

293 instance = super().create(validated_data) 

294 

295 if duplicate: 295 ↛ 296line 295 didn't jump to line 296 because the condition on line 295 was never true

296 order_id = duplicate.get('order_id', None) 

297 copy_lines = duplicate.get('copy_lines', True) 

298 copy_extra_lines = duplicate.get('copy_extra_lines', True) 

299 copy_parameters = duplicate.get('copy_parameters', True) 

300 

301 try: 

302 copy_from = instance.__class__.objects.get(pk=order_id) 

303 except Exception: 

304 # If the order ID is invalid, raise a validation error 

305 raise ValidationError(_('Invalid order ID')) 

306 

307 if copy_lines: 

308 for line in copy_from.lines.all(): 

309 instance.clean_line_item(line) 

310 line.save() 

311 

312 if copy_extra_lines: 

313 for line in copy_from.extra_lines.all(): 

314 line.pk = None 

315 line.order = instance 

316 line.save() 

317 

318 if copy_parameters: 

319 instance.copy_parameters_from(copy_from) 

320 

321 return instance 

322 

323 

324class AbstractLineItemSerializer(FilterableSerializerMixin, serializers.Serializer): 

325 """Abstract serializer for LineItem object.""" 

326 

327 @staticmethod 

328 def line_fields(extra_fields): 

329 """Construct a set of fields for this serializer.""" 

330 return [ 

331 'pk', 

332 'line', 

333 'link', 

334 'notes', 

335 'order', 

336 'project_code', 

337 'quantity', 

338 'reference', 

339 'target_date', 

340 # Filterable detail fields 

341 'order_detail', 

342 'project_code_label', 

343 'project_code_detail', 

344 *extra_fields, 

345 ] 

346 

347 target_date = serializers.DateField( 

348 required=False, allow_null=True, label=_('Target Date') 

349 ) 

350 

351 project_code_label = common.filters.enable_project_label_filter() 

352 

353 project_code_detail = common.filters.enable_project_code_filter() 

354 

355 

356class AbstractExtraLineSerializer( 

357 DataImportExportSerializerMixin, FilterableSerializerMixin, serializers.Serializer 

358): 

359 """Abstract Serializer for a ExtraLine object.""" 

360 

361 @staticmethod 

362 def extra_line_fields(extra_fields): 

363 """Construct a set of fields for this serializer.""" 

364 return [ 

365 'pk', 

366 'line', 

367 'description', 

368 'link', 

369 'notes', 

370 'order', 

371 'price', 

372 'price_currency', 

373 'project_code', 

374 'quantity', 

375 'reference', 

376 'target_date', 

377 # Filterable detail fields 

378 'order_detail', 

379 'project_code_label', 

380 'project_code_detail', 

381 *extra_fields, 

382 ] 

383 

384 quantity = serializers.FloatField() 

385 

386 price = InvenTreeMoneySerializer(allow_null=True) 

387 

388 price_currency = InvenTreeCurrencySerializer() 

389 

390 project_code_label = common.filters.enable_project_label_filter() 

391 

392 project_code_detail = common.filters.enable_project_code_filter() 

393 

394 

395class AbstractExtraLineMeta: 

396 """Abstract Meta for ExtraLine.""" 

397 

398 fields = [ 

399 'pk', 

400 'description', 

401 'quantity', 

402 'reference', 

403 'notes', 

404 'context', 

405 'order', 

406 'order_detail', 

407 'price', 

408 'price_currency', 

409 'link', 

410 ] 

411 

412 

413@register_importer() 

414class PurchaseOrderSerializer( 

415 NotesFieldMixin, 

416 TotalPriceMixin, 

417 InvenTreeCustomStatusSerializerMixin, 

418 AbstractOrderSerializer, 

419 InvenTreeModelSerializer, 

420): 

421 """Serializer for a PurchaseOrder object.""" 

422 

423 class Meta: 

424 """Metaclass options.""" 

425 

426 model = order.models.PurchaseOrder 

427 fields = AbstractOrderSerializer.order_fields([ 

428 'complete_date', 

429 'supplier', 

430 'supplier_detail', 

431 'supplier_reference', 

432 'supplier_name', 

433 'total_price', 

434 'order_currency', 

435 'destination', 

436 'updated_at', 

437 ]) 

438 read_only_fields = [ 

439 'issue_date', 

440 'complete_date', 

441 'creation_date', 

442 'updated_at', 

443 ] 

444 extra_kwargs = { 

445 'supplier': {'required': True}, 

446 'order_currency': {'required': False}, 

447 } 

448 

449 def skip_create_fields(self): 

450 """Skip these fields when instantiating a new object.""" 

451 fields = super().skip_create_fields() 

452 

453 return [*fields, 'duplicate'] 

454 

455 @staticmethod 

456 def annotate_queryset(queryset): 

457 """Add extra information to the queryset. 

458 

459 - Number of lines in the PurchaseOrder 

460 - Overdue status of the PurchaseOrder 

461 """ 

462 queryset = AbstractOrderSerializer.annotate_queryset(queryset) 

463 

464 queryset = queryset.annotate( 

465 completed_lines=SubqueryCount( 

466 'lines', filter=Q(quantity__lte=F('received')) 

467 ) 

468 ) 

469 

470 queryset = queryset.annotate( 

471 overdue=Case( 

472 When( 

473 order.models.PurchaseOrder.overdue_filter(), 

474 then=Value(True, output_field=BooleanField()), 

475 ), 

476 default=Value(False, output_field=BooleanField()), 

477 ) 

478 ) 

479 

480 queryset = queryset.prefetch_related('created_by') 

481 

482 return queryset 

483 

484 supplier_name = serializers.CharField( 

485 source='supplier.name', read_only=True, label=_('Supplier Name') 

486 ) 

487 

488 supplier_detail = OptionalField( 

489 serializer_class=CompanyBriefSerializer, 

490 serializer_kwargs={ 

491 'source': 'supplier', 

492 'many': False, 

493 'read_only': True, 

494 'allow_null': True, 

495 }, 

496 prefetch_fields=['supplier'], 

497 ) 

498 

499 

500class OrderAdjustSerializer(serializers.Serializer): 

501 """Generic serializer class for adjusting the status of an order.""" 

502 

503 class Meta: 

504 """Metaclass options. 

505 

506 By default, there are no fields required for this serializer type. 

507 """ 

508 

509 fields = [] 

510 

511 @property 

512 def order(self): 

513 """Return the order object associated with this serializer. 

514 

515 Note: It is passed in via the serializer context data. 

516 """ 

517 return self.context['order'] 

518 

519 

520class PurchaseOrderHoldSerializer(OrderAdjustSerializer): 

521 """Serializer for placing a PurchaseOrder on hold.""" 

522 

523 def save(self): 

524 """Save the serializer to 'hold' the order.""" 

525 self.order.hold_order() 

526 

527 

528class PurchaseOrderCancelSerializer(OrderAdjustSerializer): 

529 """Serializer for cancelling a PurchaseOrder.""" 

530 

531 def save(self): 

532 """Save the serializer to 'cancel' the order.""" 

533 if not self.order.can_cancel: 533 ↛ anywhereline 533 didn't jump anywhere: it always raised an exception.

534 raise ValidationError(_('Order cannot be cancelled')) 

535 

536 self.order.cancel_order() 

537 

538 

539class PurchaseOrderCompleteSerializer(OrderAdjustSerializer): 

540 """Serializer for completing a purchase order.""" 

541 

542 class Meta: 

543 """Metaclass options.""" 

544 

545 fields = ['accept_incomplete'] 

546 

547 accept_incomplete = serializers.BooleanField( 

548 label=_('Accept Incomplete'), 

549 help_text=_('Allow order to be closed with incomplete line items'), 

550 required=False, 

551 default=False, 

552 ) 

553 

554 def validate_accept_incomplete(self, value): 

555 """Check if the 'accept_incomplete' field is required.""" 

556 order = self.context['order'] 

557 

558 if not value and not order.is_complete: 

559 raise ValidationError(_('Order has incomplete line items')) 

560 

561 return value 

562 

563 def get_context_data(self): 

564 """Custom context information for this serializer.""" 

565 order = self.context['order'] 

566 

567 return {'is_complete': order.is_complete} 

568 

569 def save(self): 

570 """Save the serializer to 'complete' the order.""" 

571 self.order.complete_order() 

572 

573 

574class PurchaseOrderIssueSerializer(OrderAdjustSerializer): 

575 """Serializer for issuing (sending) a purchase order.""" 

576 

577 def save(self): 

578 """Save the serializer to 'place' the order.""" 

579 self.order.place_order() 

580 

581 

582@register_importer() 

583class PurchaseOrderLineItemSerializer( 

584 DataImportExportSerializerMixin, 

585 AbstractLineItemSerializer, 

586 InvenTreeModelSerializer, 

587): 

588 """Serializer class for the PurchaseOrderLineItem model.""" 

589 

590 class Meta: 

591 """Metaclass options.""" 

592 

593 model = order.models.PurchaseOrderLineItem 

594 fields = AbstractLineItemSerializer.line_fields([ 

595 'part', 

596 'build_order', 

597 'overdue', 

598 'received', 

599 'purchase_price', 

600 'purchase_price_currency', 

601 'auto_pricing', 

602 'destination', 

603 'total_price', 

604 'merge_items', 

605 'sku', 

606 'mpn', 

607 'ipn', 

608 'internal_part', 

609 'internal_part_name', 

610 # Filterable detail fields 

611 'build_order_detail', 

612 'destination_detail', 

613 'part_detail', 

614 'supplier_part_detail', 

615 ]) 

616 

617 def skip_create_fields(self): 

618 """Return a list of fields to skip when creating a new object.""" 

619 return ['auto_pricing', 'merge_items', *super().skip_create_fields()] 

620 

621 @staticmethod 

622 def annotate_queryset(queryset): 

623 """Add some extra annotations to this queryset. 

624 

625 - "total_price" = purchase_price * quantity 

626 - "overdue" status (boolean field) 

627 """ 

628 queryset = queryset.prefetch_related( 

629 'order', 

630 'order__responsible', 

631 'order__stock_items', 

632 'part', 

633 'part__part', 

634 'part__part__pricing_data', 

635 'part__part__default_location', 

636 'part__supplier', 

637 'part__manufacturer_part', 

638 'part__manufacturer_part__manufacturer', 

639 ) 

640 

641 queryset = queryset.annotate( 

642 total_price=ExpressionWrapper( 

643 F('purchase_price') * F('quantity'), output_field=models.DecimalField() 

644 ) 

645 ) 

646 

647 queryset = queryset.annotate( 

648 overdue=Case( 

649 When( 

650 order.models.PurchaseOrderLineItem.get_overdue_filter(), 

651 then=Value(True, output_field=BooleanField()), 

652 ), 

653 default=Value(False, output_field=BooleanField()), 

654 ) 

655 ) 

656 

657 return queryset 

658 

659 part = serializers.PrimaryKeyRelatedField( 

660 queryset=company_models.SupplierPart.objects.all(), 

661 many=False, 

662 required=True, 

663 allow_null=True, 

664 label=_('Supplier Part'), 

665 ) 

666 

667 quantity = serializers.FloatField(min_value=0, required=True) 

668 

669 def validate_quantity(self, quantity): 

670 """Validation for the 'quantity' field.""" 

671 if quantity <= 0: 

672 raise ValidationError(_('Quantity must be greater than zero')) 

673 

674 return quantity 

675 

676 def validate_purchase_order(self, purchase_order): 

677 """Validation for the 'purchase_order' field.""" 

678 if purchase_order.status not in PurchaseOrderStatusGroups.OPEN: 

679 raise ValidationError(_('Order is not open')) 

680 

681 return purchase_order 

682 

683 received = serializers.FloatField(default=0, read_only=True) 

684 

685 overdue = serializers.BooleanField(read_only=True, allow_null=True) 

686 

687 total_price = serializers.FloatField(read_only=True) 

688 

689 part_detail = OptionalField( 

690 serializer_class=PartBriefSerializer, 

691 serializer_kwargs={ 

692 'source': 'get_base_part', 

693 'many': False, 

694 'read_only': True, 

695 'allow_null': True, 

696 }, 

697 default_include=False, 

698 filter_name='part_detail', 

699 ) 

700 

701 supplier_part_detail = OptionalField( 

702 serializer_class=SupplierPartSerializer, 

703 serializer_kwargs={ 

704 'source': 'part', 

705 'brief': True, 

706 'many': False, 

707 'read_only': True, 

708 'allow_null': True, 

709 }, 

710 default_include=False, 

711 filter_name='part_detail', 

712 ) 

713 

714 purchase_price = InvenTreeMoneySerializer(allow_null=True) 

715 

716 auto_pricing = serializers.BooleanField( 

717 label=_('Auto Pricing'), 

718 help_text=_( 

719 'Automatically calculate purchase price based on supplier part data' 

720 ), 

721 default=False, 

722 ) 

723 

724 destination_detail = OptionalField( 

725 serializer_class=stock.serializers.LocationBriefSerializer, 

726 serializer_kwargs={ 

727 'source': 'get_destination', 

728 'read_only': True, 

729 'allow_null': True, 

730 }, 

731 default_include=True, 

732 prefetch_fields=['destination', 'order__destination'], 

733 ) 

734 

735 purchase_price_currency = InvenTreeCurrencySerializer( 

736 help_text=_('Purchase price currency') 

737 ) 

738 

739 order_detail = OptionalField( 

740 serializer_class=PurchaseOrderSerializer, 

741 serializer_kwargs={ 

742 'source': 'order', 

743 'read_only': True, 

744 'allow_null': True, 

745 'many': False, 

746 }, 

747 default_include=True, 

748 ) 

749 

750 build_order_detail = OptionalField( 

751 serializer_class=build.serializers.BuildSerializer, 

752 serializer_kwargs={ 

753 'source': 'build_order', 

754 'read_only': True, 

755 'allow_null': True, 

756 'many': False, 

757 }, 

758 default_include=True, 

759 prefetch_fields=[ 

760 'build_order__responsible', 

761 'build_order__issued_by', 

762 'build_order__part', 

763 ], 

764 ) 

765 

766 merge_items = serializers.BooleanField( 

767 label=_('Merge Items'), 

768 help_text=_( 

769 'Merge items with the same part, destination and target date into one line item' 

770 ), 

771 default=True, 

772 write_only=True, 

773 ) 

774 

775 sku = serializers.CharField( 

776 source='part.SKU', read_only=True, allow_null=True, label=_('SKU') 

777 ) 

778 

779 mpn = serializers.CharField( 

780 source='part.manufacturer_part.MPN', 

781 read_only=True, 

782 allow_null=True, 

783 label=_('MPN'), 

784 ) 

785 

786 ipn = serializers.CharField( 

787 source='part.part.IPN', 

788 read_only=True, 

789 allow_null=True, 

790 label=_('Internal Part Number'), 

791 ) 

792 

793 internal_part = serializers.PrimaryKeyRelatedField( 

794 source='part.part', read_only=True, many=False, label=_('Internal Part') 

795 ) 

796 

797 internal_part_name = serializers.CharField( 

798 source='part.part.name', read_only=True, label=_('Internal Part Name') 

799 ) 

800 

801 def validate(self, data): 

802 """Custom validation for the serializer. 

803 

804 - Ensure the supplier_part field is supplied 

805 - Ensure the purchase_order field is supplied 

806 - Ensure that the supplier_part and supplier references match 

807 """ 

808 data = super().validate(data) 

809 

810 supplier_part = data.get('part', None) 

811 purchase_order = data.get('order', None) 

812 

813 if not supplier_part: 

814 raise ValidationError({'part': _('Supplier part must be specified')}) 

815 

816 if not purchase_order: 

817 raise ValidationError({'order': _('Purchase order must be specified')}) 

818 

819 # Check that the supplier part and purchase order match 

820 if ( 

821 supplier_part is not None 

822 and supplier_part.supplier != purchase_order.supplier 

823 ): 

824 raise ValidationError({ 

825 'part': _('Supplier must match purchase order'), 

826 'order': _('Purchase order must match supplier'), 

827 }) 

828 

829 return data 

830 

831 

832@register_importer() 

833class PurchaseOrderExtraLineSerializer( 

834 AbstractExtraLineSerializer, InvenTreeModelSerializer 

835): 

836 """Serializer for a PurchaseOrderExtraLine object.""" 

837 

838 class Meta(AbstractExtraLineMeta): 

839 """Metaclass options.""" 

840 

841 model = order.models.PurchaseOrderExtraLine 

842 fields = AbstractExtraLineSerializer.extra_line_fields([]) 

843 

844 order_detail = OptionalField( 

845 serializer_class=PurchaseOrderSerializer, 

846 serializer_kwargs={ 

847 'source': 'order', 

848 'many': False, 

849 'read_only': True, 

850 'allow_null': True, 

851 }, 

852 ) 

853 

854 

855class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer): 

856 """A serializer for receiving a single purchase order line item against a purchase order.""" 

857 

858 class Meta: 

859 """Metaclass options.""" 

860 

861 fields = [ 

862 'barcode', 

863 'line_item', 

864 'location', 

865 'quantity', 

866 'status', 

867 'batch_code', 

868 'expiry_date', 

869 'serial_numbers', 

870 'packaging', 

871 'note', 

872 ] 

873 

874 line_item = serializers.PrimaryKeyRelatedField( 

875 queryset=order.models.PurchaseOrderLineItem.objects.all(), 

876 allow_null=False, 

877 required=True, 

878 label=_('Line Item'), 

879 ) 

880 

881 location = serializers.PrimaryKeyRelatedField( 

882 queryset=stock.models.StockLocation.objects.all(), 

883 many=False, 

884 allow_null=True, 

885 required=False, 

886 label=_('Location'), 

887 help_text=_('Select destination location for received items'), 

888 ) 

889 

890 quantity = serializers.DecimalField( 

891 max_digits=15, decimal_places=5, min_value=Decimal(0), required=True 

892 ) 

893 

894 def validate_quantity(self, quantity): 

895 """Validation for the 'quantity' field.""" 

896 if quantity <= 0: 896 ↛ 897line 896 didn't jump to line 897 because the condition on line 896 was never true

897 raise ValidationError(_('Quantity must be greater than zero')) 

898 

899 return quantity 

900 

901 batch_code = serializers.CharField( 

902 label=_('Batch Code'), 

903 help_text=_('Enter batch code for incoming stock items'), 

904 required=False, 

905 default='', 

906 allow_blank=True, 

907 ) 

908 

909 expiry_date = serializers.DateField( 

910 label=_('Expiry Date'), 

911 help_text=_('Enter expiry date for incoming stock items'), 

912 required=False, 

913 allow_null=True, 

914 default=None, 

915 ) 

916 

917 serial_numbers = serializers.CharField( 

918 label=_('Serial Numbers'), 

919 help_text=_('Enter serial numbers for incoming stock items'), 

920 required=False, 

921 default='', 

922 allow_blank=True, 

923 ) 

924 

925 status = stock.serializers.StockStatusCustomSerializer(default=StockStatus.OK.value) 

926 

927 packaging = serializers.CharField( 

928 label=_('Packaging'), 

929 help_text=_('Override packaging information for incoming stock items'), 

930 required=False, 

931 default='', 

932 allow_blank=True, 

933 ) 

934 

935 note = serializers.CharField( 

936 label=_('Note'), 

937 help_text=_('Additional note for incoming stock items'), 

938 required=False, 

939 default='', 

940 allow_blank=True, 

941 ) 

942 

943 barcode = serializers.CharField( 

944 label=_('Barcode'), 

945 help_text=_('Scanned barcode'), 

946 default='', 

947 required=False, 

948 allow_null=True, 

949 allow_blank=True, 

950 ) 

951 

952 def validate_barcode(self, barcode): 

953 """Cannot check in a LineItem with a barcode that is already assigned.""" 

954 # Ignore empty barcode values 

955 if not barcode or barcode.strip() == '': 955 ↛ 958line 955 didn't jump to line 958 because the condition on line 955 was always true

956 return None 

957 

958 barcode_hash = hash_barcode(barcode) 

959 

960 if stock.models.StockItem.lookup_barcode(barcode_hash) is not None: 

961 raise ValidationError(_('Barcode is already in use')) 

962 

963 return barcode 

964 

965 def validate(self, data): 

966 """Custom validation for the serializer. 

967 

968 - Integer quantity must be provided for serialized stock 

969 - Validate serial numbers (if provided) 

970 """ 

971 data = super().validate(data) 

972 

973 line_item = data['line_item'] 

974 quantity = data['quantity'] 

975 serial_numbers = data.get('serial_numbers', '').strip() 

976 

977 # If serial numbers are provided 

978 if serial_numbers: 

979 supplier_part = line_item.part 

980 base_part = supplier_part.part 

981 base_quantity = supplier_part.base_quantity(quantity) 

982 

983 try: 

984 # Pass the serial numbers through to the parent serializer once validated 

985 data['serials'] = extract_serial_numbers( 

986 serial_numbers, 

987 base_quantity, 

988 base_part.get_latest_serial_number(), 

989 part=base_part, 

990 ) 

991 except DjangoValidationError as e: 

992 raise ValidationError({'serial_numbers': e.messages}) 

993 

994 invalid_serials = [] 

995 

996 # Check the serial numbers are valid 

997 for serial in data['serials']: 

998 try: 

999 base_part.validate_serial_number(serial, raise_error=True) 

1000 except (ValidationError, DjangoValidationError): 

1001 invalid_serials.append(serial) 

1002 

1003 if len(invalid_serials) > 0: 

1004 msg = _('The following serial numbers already exist or are invalid') 

1005 msg += ': ' + ', '.join(invalid_serials) 

1006 raise ValidationError({'serial_numbers': msg}) 

1007 

1008 return data 

1009 

1010 

1011class PurchaseOrderReceiveSerializer(serializers.Serializer): 

1012 """Serializer for receiving items against a PurchaseOrder.""" 

1013 

1014 class Meta: 

1015 """Metaclass options.""" 

1016 

1017 fields = ['items', 'location'] 

1018 

1019 items = PurchaseOrderLineItemReceiveSerializer(many=True) 

1020 

1021 location = serializers.PrimaryKeyRelatedField( 

1022 queryset=stock.models.StockLocation.objects.all(), 

1023 many=False, 

1024 required=False, 

1025 allow_null=True, 

1026 label=_('Location'), 

1027 help_text=_('Select destination location for received items'), 

1028 ) 

1029 

1030 def validate(self, data): 

1031 """Custom validation for the serializer. 

1032 

1033 - Ensure line items are provided 

1034 - Check that a location is specified 

1035 """ 

1036 super().validate(data) 

1037 

1038 order = self.context['order'] 

1039 items = data.get('items', []) 

1040 

1041 location = data.get('location', order.destination) 

1042 

1043 if len(items) == 0: 

1044 raise ValidationError(_('Line items must be provided')) 

1045 

1046 # Ensure barcodes are unique 

1047 unique_barcodes = set() 

1048 

1049 # Check if the location is not specified for any particular item 

1050 for item in items: 

1051 line = item['line_item'] 

1052 

1053 if not item.get('location', None): 

1054 # If a global location is specified, use that 

1055 item['location'] = location 

1056 

1057 if not item['location']: 

1058 # The line item specifies a location? 

1059 item['location'] = line.get_destination() 

1060 

1061 if not item['location']: 

1062 raise ValidationError({ 

1063 'location': _('Destination location must be specified') 

1064 }) 

1065 

1066 barcode = item.get('barcode', '') 

1067 

1068 if barcode: 

1069 if barcode in unique_barcodes: 

1070 raise ValidationError(_('Supplied barcode values must be unique')) 

1071 else: 

1072 unique_barcodes.add(barcode) 

1073 

1074 return data 

1075 

1076 def save(self) -> list[stock.models.StockItem]: 

1077 """Perform the actual database transaction to receive purchase order items.""" 

1078 data = self.validated_data 

1079 

1080 request = self.context.get('request') 

1081 order = self.context['order'] 

1082 

1083 items = data['items'] 

1084 

1085 # Location can be provided, or default to the order destination 

1086 location = data.get('location', order.destination) 

1087 

1088 try: 

1089 items = order.receive_line_items( 

1090 location, items, request.user if request else None 

1091 ) 

1092 except (ValidationError, DjangoValidationError) as exc: 

1093 # Catch model errors and re-throw as DRF errors 

1094 raise ValidationError(detail=serializers.as_serializer_error(exc)) 

1095 

1096 # Returns a list of the created items 

1097 return items 

1098 

1099 

1100@register_importer() 

1101class SalesOrderSerializer( 

1102 NotesFieldMixin, 

1103 TotalPriceMixin, 

1104 InvenTreeCustomStatusSerializerMixin, 

1105 AbstractOrderSerializer, 

1106 InvenTreeTaggitSerializer, 

1107 InvenTreeModelSerializer, 

1108): 

1109 """Serializer for the SalesOrder model class.""" 

1110 

1111 class Meta: 

1112 """Metaclass options.""" 

1113 

1114 model = order.models.SalesOrder 

1115 fields = AbstractOrderSerializer.order_fields([ 

1116 'customer', 

1117 'customer_detail', 

1118 'customer_reference', 

1119 'shipment_date', 

1120 'total_price', 

1121 'order_currency', 

1122 'shipments_count', 

1123 'completed_shipments_count', 

1124 'allocated_lines', 

1125 'updated_at', 

1126 'tags', 

1127 ]) 

1128 read_only_fields = ['status', 'creation_date', 'shipment_date', 'updated_at'] 

1129 extra_kwargs = {'order_currency': {'required': False}} 

1130 

1131 def skip_create_fields(self): 

1132 """Skip these fields when instantiating a new object.""" 

1133 fields = super().skip_create_fields() 

1134 

1135 return [*fields, 'duplicate'] 

1136 

1137 @staticmethod 

1138 def annotate_queryset(queryset): 

1139 """Add extra information to the queryset. 

1140 

1141 - Number of line items in the SalesOrder 

1142 - Number of fully allocated line items 

1143 - Number of completed line items in the SalesOrder 

1144 - Overdue status of the SalesOrder 

1145 """ 

1146 queryset = AbstractOrderSerializer.annotate_queryset(queryset) 

1147 

1148 queryset = queryset.annotate( 

1149 completed_lines=SubqueryCount('lines', filter=Q(quantity__lte=F('shipped'))) 

1150 ) 

1151 

1152 queryset = queryset.annotate( 

1153 allocated_lines=SubqueryCount( 

1154 'lines', 

1155 filter=Q(part__virtual=True) 

1156 | Q(shipped__gte=F('quantity')) 

1157 | Q( 

1158 quantity__lte=Coalesce( 

1159 SubquerySum('allocations__quantity'), Decimal(0) 

1160 ) 

1161 ), 

1162 ) 

1163 ) 

1164 

1165 queryset = queryset.annotate( 

1166 overdue=Case( 

1167 When( 

1168 order.models.SalesOrder.overdue_filter(), 

1169 then=Value(True, output_field=BooleanField()), 

1170 ), 

1171 default=Value(False, output_field=BooleanField()), 

1172 ) 

1173 ) 

1174 

1175 # Annotate shipment details 

1176 queryset = queryset.annotate( 

1177 shipments_count=SubqueryCount('shipments'), 

1178 completed_shipments_count=SubqueryCount( 

1179 'shipments', filter=Q(shipment_date__isnull=False) 

1180 ), 

1181 ) 

1182 

1183 return queryset 

1184 

1185 customer_detail = OptionalField( 

1186 serializer_class=CompanyBriefSerializer, 

1187 serializer_kwargs={ 

1188 'source': 'customer', 

1189 'many': False, 

1190 'read_only': True, 

1191 'allow_null': True, 

1192 }, 

1193 prefetch_fields=['customer'], 

1194 ) 

1195 

1196 shipments_count = serializers.IntegerField( 

1197 read_only=True, allow_null=True, label=_('Shipments') 

1198 ) 

1199 

1200 completed_shipments_count = serializers.IntegerField( 

1201 read_only=True, allow_null=True, label=_('Completed Shipments') 

1202 ) 

1203 

1204 allocated_lines = serializers.IntegerField( 

1205 read_only=True, allow_null=True, label=_('Allocated Lines') 

1206 ) 

1207 

1208 tags = common.filters.enable_tags_filter() 

1209 

1210 

1211class SalesOrderIssueSerializer(OrderAdjustSerializer): 

1212 """Serializer for issuing a SalesOrder.""" 

1213 

1214 def save(self): 

1215 """Save the serializer to 'issue' the order.""" 

1216 self.order.issue_order() 

1217 

1218 

1219@register_importer() 

1220class SalesOrderLineItemSerializer( 

1221 DataImportExportSerializerMixin, 

1222 AbstractLineItemSerializer, 

1223 InvenTreeModelSerializer, 

1224): 

1225 """Serializer for a SalesOrderLineItem object.""" 

1226 

1227 class Meta: 

1228 """Metaclass options.""" 

1229 

1230 model = order.models.SalesOrderLineItem 

1231 fields = AbstractLineItemSerializer.line_fields([ 

1232 'allocated', 

1233 'customer_detail', 

1234 'overdue', 

1235 'part', 

1236 'part_detail', 

1237 'sale_price', 

1238 'sale_price_currency', 

1239 'shipped', 

1240 # Annotated fields for part stocking information 

1241 'available_stock', 

1242 'available_variant_stock', 

1243 'building', 

1244 'on_order', 

1245 # Filterable detail fields 

1246 ]) 

1247 

1248 @staticmethod 

1249 def annotate_queryset(queryset): 

1250 """Add some extra annotations to this queryset. 

1251 

1252 - "overdue" status (boolean field) 

1253 - "available_quantity" 

1254 - "building" 

1255 - "on_order" 

1256 """ 

1257 queryset = queryset.annotate( 

1258 overdue=Case( 

1259 When( 

1260 Q(order__status__in=SalesOrderStatusGroups.OPEN) 

1261 & order.models.SalesOrderLineItem.get_overdue_filter(), 

1262 then=Value(True, output_field=BooleanField()), 

1263 ), 

1264 default=Value(False, output_field=BooleanField()), 

1265 ) 

1266 ) 

1267 

1268 # Annotate each line with the available stock quantity 

1269 # To do this, we need to look at the total stock and any allocations 

1270 queryset = queryset.alias( 

1271 total_stock=part_filters.annotate_total_stock(reference='part__'), 

1272 allocated_to_sales_orders=part_filters.annotate_sales_order_allocations( 

1273 reference='part__' 

1274 ), 

1275 allocated_to_build_orders=part_filters.annotate_build_order_allocations( 

1276 reference='part__' 

1277 ), 

1278 ) 

1279 

1280 queryset = queryset.annotate( 

1281 available_stock=Greatest( 

1282 ExpressionWrapper( 

1283 F('total_stock') 

1284 - F('allocated_to_sales_orders') 

1285 - F('allocated_to_build_orders'), 

1286 output_field=models.DecimalField(), 

1287 ), 

1288 0, 

1289 output_field=models.DecimalField(), 

1290 ) 

1291 ) 

1292 

1293 # Filter for "variant" stock: Variant stock items must be salable and active 

1294 variant_stock_query = part_filters.variant_stock_query( 

1295 reference='part__' 

1296 ).filter(part__salable=True, part__active=True) 

1297 

1298 # Also add in available "variant" stock 

1299 queryset = queryset.alias( 

1300 variant_stock_total=part_filters.annotate_variant_quantity( 

1301 variant_stock_query, reference='quantity' 

1302 ), 

1303 variant_bo_allocations=part_filters.annotate_variant_quantity( 

1304 variant_stock_query, reference='sales_order_allocations__quantity' 

1305 ), 

1306 variant_so_allocations=part_filters.annotate_variant_quantity( 

1307 variant_stock_query, reference='allocations__quantity' 

1308 ), 

1309 ) 

1310 

1311 queryset = queryset.annotate( 

1312 available_variant_stock=Greatest( 

1313 ExpressionWrapper( 

1314 F('variant_stock_total') 

1315 - F('variant_bo_allocations') 

1316 - F('variant_so_allocations'), 

1317 output_field=models.DecimalField(), 

1318 ), 

1319 0, 

1320 output_field=models.DecimalField(), 

1321 ) 

1322 ) 

1323 

1324 # Add information about the quantity of parts currently on order 

1325 queryset = queryset.annotate( 

1326 on_order=part_filters.annotate_on_order_quantity(reference='part__') 

1327 ) 

1328 

1329 # Add information about the quantity of parts currently in production 

1330 queryset = queryset.annotate( 

1331 building=part_filters.annotate_in_production_quantity(reference='part__') 

1332 ) 

1333 

1334 # Annotate total 'allocated' stock quantity 

1335 queryset = queryset.annotate( 

1336 allocated=Coalesce( 

1337 SubquerySum('allocations__quantity'), 

1338 Decimal(0), 

1339 output_field=models.DecimalField(), 

1340 ) 

1341 ) 

1342 

1343 return queryset 

1344 

1345 order_detail = OptionalField( 

1346 serializer_class=SalesOrderSerializer, 

1347 serializer_kwargs={ 

1348 'source': 'order', 

1349 'many': False, 

1350 'read_only': True, 

1351 'allow_null': True, 

1352 }, 

1353 prefetch_fields=[ 

1354 'order__created_by', 

1355 'order__responsible', 

1356 'order__address', 

1357 'order__project_code', 

1358 'order__contact', 

1359 ], 

1360 ) 

1361 

1362 part_detail = OptionalField( 

1363 serializer_class=PartBriefSerializer, 

1364 serializer_kwargs={ 

1365 'source': 'part', 

1366 'many': False, 

1367 'read_only': True, 

1368 'allow_null': True, 

1369 }, 

1370 prefetch_fields=['part__pricing_data'], 

1371 ) 

1372 

1373 customer_detail = OptionalField( 

1374 serializer_class=CompanyBriefSerializer, 

1375 serializer_kwargs={ 

1376 'source': 'order.customer', 

1377 'many': False, 

1378 'read_only': True, 

1379 'allow_null': True, 

1380 }, 

1381 prefetch_fields=['order__customer'], 

1382 ) 

1383 

1384 # Annotated fields 

1385 overdue = serializers.BooleanField(read_only=True, allow_null=True) 

1386 available_stock = serializers.FloatField(read_only=True) 

1387 available_variant_stock = serializers.FloatField(read_only=True) 

1388 on_order = serializers.FloatField(label=_('On Order'), read_only=True) 

1389 building = serializers.FloatField(label=_('In Production'), read_only=True) 

1390 

1391 quantity = InvenTreeDecimalField() 

1392 

1393 allocated = serializers.FloatField(read_only=True) 

1394 

1395 shipped = InvenTreeDecimalField(read_only=True) 

1396 

1397 sale_price = InvenTreeMoneySerializer(allow_null=True) 

1398 

1399 sale_price_currency = InvenTreeCurrencySerializer( 

1400 help_text=_('Sale price currency') 

1401 ) 

1402 

1403 

1404@register_importer() 

1405class SalesOrderShipmentSerializer( 

1406 DataImportExportSerializerMixin, 

1407 FilterableSerializerMixin, 

1408 InvenTreeTaggitSerializer, 

1409 NotesFieldMixin, 

1410 InvenTreeModelSerializer, 

1411): 

1412 """Serializer for the SalesOrderShipment class.""" 

1413 

1414 class Meta: 

1415 """Metaclass options.""" 

1416 

1417 model = order.models.SalesOrderShipment 

1418 fields = [ 

1419 'pk', 

1420 'order', 

1421 'allocated_items', 

1422 'shipment_date', 

1423 'shipment_address', 

1424 'delivery_date', 

1425 'checked_by', 

1426 'reference', 

1427 'tracking_number', 

1428 'invoice_number', 

1429 'barcode_hash', 

1430 'link', 

1431 'notes', 

1432 # Extra detail fields 

1433 'parameters', 

1434 'checked_by_detail', 

1435 'customer_detail', 

1436 'order_detail', 

1437 'shipment_address_detail', 

1438 'tags', 

1439 ] 

1440 

1441 @staticmethod 

1442 def annotate_queryset(queryset): 

1443 """Annotate the queryset with extra information.""" 

1444 queryset = queryset.annotate(allocated_items=SubqueryCount('allocations')) 

1445 

1446 return queryset 

1447 

1448 allocated_items = serializers.IntegerField( 

1449 read_only=True, allow_null=True, label=_('Allocated Items') 

1450 ) 

1451 

1452 checked_by_detail = OptionalField( 

1453 serializer_class=UserSerializer, 

1454 serializer_kwargs={ 

1455 'source': 'checked_by', 

1456 'many': False, 

1457 'read_only': True, 

1458 'allow_null': True, 

1459 }, 

1460 default_include=True, 

1461 prefetch_fields=['checked_by'], 

1462 ) 

1463 

1464 order_detail = OptionalField( 

1465 serializer_class=SalesOrderSerializer, 

1466 serializer_kwargs={ 

1467 'source': 'order', 

1468 'many': False, 

1469 'read_only': True, 

1470 'allow_null': True, 

1471 }, 

1472 default_include=True, 

1473 prefetch_fields=[ 

1474 'order', 

1475 'order__customer', 

1476 'order__created_by', 

1477 'order__responsible', 

1478 'order__project_code', 

1479 ], 

1480 ) 

1481 

1482 customer_detail = OptionalField( 

1483 serializer_class=CompanyBriefSerializer, 

1484 serializer_kwargs={ 

1485 'source': 'order.customer', 

1486 'many': False, 

1487 'read_only': True, 

1488 'allow_null': True, 

1489 }, 

1490 default_include=False, 

1491 prefetch_fields=['order__customer'], 

1492 ) 

1493 

1494 shipment_address_detail = OptionalField( 

1495 serializer_class=AddressBriefSerializer, 

1496 serializer_kwargs={ 

1497 'source': 'shipment_address', 

1498 'many': False, 

1499 'read_only': True, 

1500 'allow_null': True, 

1501 }, 

1502 default_include=True, 

1503 prefetch_fields=['shipment_address'], 

1504 ) 

1505 

1506 parameters = common.filters.enable_parameters_filter() 

1507 

1508 tags = common.filters.enable_tags_filter() 

1509 

1510 

1511class SalesOrderAllocationSerializer( 

1512 FilterableSerializerMixin, InvenTreeModelSerializer 

1513): 

1514 """Serializer for the SalesOrderAllocation model. 

1515 

1516 This includes some fields from the related model objects. 

1517 """ 

1518 

1519 class Meta: 

1520 """Metaclass options.""" 

1521 

1522 model = order.models.SalesOrderAllocation 

1523 fields = [ 

1524 'pk', 

1525 'item', 

1526 'quantity', 

1527 'shipment', 

1528 # Annotated read-only fields 

1529 'line', 

1530 'part', 

1531 'order', 

1532 'serial', 

1533 'location', 

1534 # Extra detail fields 

1535 'item_detail', 

1536 'part_detail', 

1537 'order_detail', 

1538 'customer_detail', 

1539 'location_detail', 

1540 'shipment_detail', 

1541 ] 

1542 read_only_fields = ['line', ''] 

1543 

1544 part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) 

1545 order = serializers.PrimaryKeyRelatedField( 

1546 source='line.order', many=False, read_only=True 

1547 ) 

1548 serial = serializers.CharField(source='get_serial', read_only=True, allow_null=True) 

1549 quantity = serializers.FloatField(read_only=False) 

1550 location = serializers.PrimaryKeyRelatedField( 

1551 source='item.location', many=False, read_only=True 

1552 ) 

1553 

1554 # Extra detail fields 

1555 order_detail = OptionalField( 

1556 serializer_class=SalesOrderSerializer, 

1557 serializer_kwargs={ 

1558 'source': 'line.order', 

1559 'many': False, 

1560 'read_only': True, 

1561 'allow_null': True, 

1562 }, 

1563 ) 

1564 

1565 part_detail = OptionalField( 

1566 serializer_class=PartBriefSerializer, 

1567 serializer_kwargs={ 

1568 'source': 'item.part', 

1569 'many': False, 

1570 'read_only': True, 

1571 'allow_null': True, 

1572 }, 

1573 default_include=True, 

1574 ) 

1575 

1576 item_detail = OptionalField( 

1577 serializer_class=stock.serializers.StockItemSerializer, 

1578 serializer_kwargs={ 

1579 'source': 'item', 

1580 'many': False, 

1581 'read_only': True, 

1582 'allow_null': True, 

1583 'part_detail': False, 

1584 'location_detail': False, 

1585 'supplier_part_detail': False, 

1586 }, 

1587 default_include=True, 

1588 ) 

1589 location_detail = OptionalField( 

1590 serializer_class=stock.serializers.LocationBriefSerializer, 

1591 serializer_kwargs={ 

1592 'source': 'item.location', 

1593 'many': False, 

1594 'read_only': True, 

1595 'allow_null': True, 

1596 }, 

1597 ) 

1598 

1599 customer_detail = OptionalField( 

1600 serializer_class=CompanyBriefSerializer, 

1601 serializer_kwargs={ 

1602 'source': 'line.order.customer', 

1603 'many': False, 

1604 'read_only': True, 

1605 'allow_null': True, 

1606 }, 

1607 ) 

1608 

1609 shipment_detail = OptionalField( 

1610 serializer_class=SalesOrderShipmentSerializer, 

1611 serializer_kwargs={ 

1612 'source': 'shipment', 

1613 'order_detail': False, 

1614 'many': False, 

1615 'read_only': True, 

1616 'allow_null': True, 

1617 }, 

1618 default_include=True, 

1619 prefetch_fields=['shipment'], 

1620 ) 

1621 

1622 

1623class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer): 

1624 """Serializer for completing (shipping) a SalesOrderShipment.""" 

1625 

1626 class Meta: 

1627 """Metaclass options.""" 

1628 

1629 model = order.models.SalesOrderShipment 

1630 

1631 fields = [ 

1632 'shipment_date', 

1633 'delivery_date', 

1634 'tracking_number', 

1635 'invoice_number', 

1636 'link', 

1637 ] 

1638 

1639 def validate(self, data): 

1640 """Custom validation for the serializer. 

1641 

1642 - Ensure the shipment reference is provided 

1643 """ 

1644 data = super().validate(data) 

1645 

1646 shipment = self.context.get('shipment', None) 

1647 

1648 if not shipment: 

1649 raise ValidationError(_('No shipment details provided')) 

1650 

1651 shipment.check_can_complete(raise_error=True) 

1652 

1653 return data 

1654 

1655 

1656class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer): 

1657 """A serializer for allocating a single stock-item against a SalesOrder shipment.""" 

1658 

1659 class Meta: 

1660 """Metaclass options.""" 

1661 

1662 fields = ['line_item', 'stock_item', 'quantity'] 

1663 

1664 line_item = serializers.PrimaryKeyRelatedField( 

1665 queryset=order.models.SalesOrderLineItem.objects.all(), 

1666 many=False, 

1667 allow_null=False, 

1668 required=True, 

1669 label=_('Stock Item'), 

1670 ) 

1671 

1672 def validate_line_item(self, line_item): 

1673 """Custom validation for the 'line_item' field. 

1674 

1675 - Ensure the line_item is associated with the particular SalesOrder 

1676 """ 

1677 order = self.context['order'] 

1678 

1679 # Ensure that the line item points to the correct order 

1680 if line_item.order != order: 

1681 raise ValidationError(_('Line item is not associated with this order')) 

1682 

1683 return line_item 

1684 

1685 stock_item = serializers.PrimaryKeyRelatedField( 

1686 queryset=stock.models.StockItem.objects.all(), 

1687 many=False, 

1688 allow_null=False, 

1689 required=True, 

1690 label=_('Stock Item'), 

1691 ) 

1692 

1693 quantity = serializers.DecimalField( 

1694 max_digits=15, decimal_places=5, min_value=Decimal(0), required=True 

1695 ) 

1696 

1697 def validate_quantity(self, quantity): 

1698 """Custom validation for the 'quantity' field.""" 

1699 if quantity <= 0: 1699 ↛ 1700line 1699 didn't jump to line 1700 because the condition on line 1699 was never true

1700 raise ValidationError(_('Quantity must be positive')) 

1701 

1702 return quantity 

1703 

1704 def validate(self, data): 

1705 """Custom validation for the serializer. 

1706 

1707 - Ensure that the quantity is 1 for serialized stock 

1708 - Quantity cannot exceed the available amount 

1709 """ 

1710 data = super().validate(data) 

1711 

1712 stock_item = data['stock_item'] 

1713 quantity = data['quantity'] 

1714 

1715 if get_global_setting('SALESORDER_BLOCK_INCOMPLETE_ITEM_TESTS'): 

1716 if ( 

1717 stock_item.hasRequiredTests() 

1718 and not stock_item.passedAllRequiredTests() 

1719 ): 

1720 raise ValidationError({ 

1721 'stock_item': _('Stock item has not passed all required tests') 

1722 }) 

1723 

1724 if stock_item.serialized and quantity != 1: 

1725 raise ValidationError({ 

1726 'quantity': _('Quantity must be 1 for serialized stock item') 

1727 }) 

1728 

1729 q = normalize(stock_item.unallocated_quantity()) 

1730 

1731 if quantity > q: 

1732 raise ValidationError({'quantity': _(f'Available quantity ({q}) exceeded')}) 

1733 

1734 return data 

1735 

1736 

1737class SalesOrderCompleteSerializer(OrderAdjustSerializer): 

1738 """DRF serializer for manually marking a sales order as complete.""" 

1739 

1740 class Meta: 

1741 """Serializer metaclass options.""" 

1742 

1743 fields = ['accept_incomplete'] 

1744 

1745 accept_incomplete = serializers.BooleanField( 

1746 label=_('Accept Incomplete'), 

1747 help_text=_('Allow order to be closed with incomplete line items'), 

1748 required=False, 

1749 default=False, 

1750 ) 

1751 

1752 def validate_accept_incomplete(self, value): 

1753 """Check if the 'accept_incomplete' field is required.""" 

1754 order = self.context['order'] 

1755 

1756 if not value and not order.is_completed(): 

1757 raise ValidationError(_('Order has incomplete line items')) 

1758 

1759 return value 

1760 

1761 def get_context_data(self): 

1762 """Custom context data for this serializer.""" 

1763 order = self.context['order'] 

1764 

1765 return { 

1766 'is_complete': order.is_completed(), 

1767 'pending_shipments': order.pending_shipment_count, 

1768 } 

1769 

1770 def validate(self, data): 

1771 """Custom validation for the serializer.""" 

1772 data = super().validate(data) 

1773 self.order.can_complete( 

1774 raise_error=True, 

1775 allow_incomplete_lines=str2bool(data.get('accept_incomplete', False)), 

1776 ) 

1777 

1778 return data 

1779 

1780 def save(self): 

1781 """Save the serializer to complete the SalesOrder.""" 

1782 request = self.context.get('request') 

1783 data = self.validated_data 

1784 

1785 user = request.user if request else None 

1786 

1787 self.order.ship_order( 

1788 user, allow_incomplete_lines=str2bool(data.get('accept_incomplete', False)) 

1789 ) 

1790 

1791 

1792class SalesOrderHoldSerializer(OrderAdjustSerializer): 

1793 """Serializer for placing a SalesOrder on hold.""" 

1794 

1795 def save(self): 

1796 """Save the serializer to place the SalesOrder on hold.""" 

1797 self.order.hold_order() 

1798 

1799 

1800class SalesOrderCancelSerializer(OrderAdjustSerializer): 

1801 """Serializer for marking a SalesOrder as cancelled.""" 

1802 

1803 def get_context_data(self): 

1804 """Add extra context data to the serializer.""" 

1805 order = self.context['order'] 

1806 

1807 return {'can_cancel': order.can_cancel} 

1808 

1809 def save(self): 

1810 """Save the serializer to cancel the order.""" 

1811 self.order.cancel_order() 

1812 

1813 

1814class SalesOrderSerialAllocationSerializer(serializers.Serializer): 

1815 """DRF serializer for allocation of serial numbers against a sales order / shipment.""" 

1816 

1817 class Meta: 

1818 """Metaclass options.""" 

1819 

1820 fields = ['line_item', 'quantity', 'serial_numbers', 'shipment'] 

1821 

1822 line_item = serializers.PrimaryKeyRelatedField( 

1823 queryset=order.models.SalesOrderLineItem.objects.all(), 

1824 many=False, 

1825 required=True, 

1826 allow_null=False, 

1827 label=_('Line Item'), 

1828 ) 

1829 

1830 def validate_line_item(self, line_item): 

1831 """Ensure that the line_item is valid.""" 

1832 order = self.context['order'] 

1833 

1834 # Ensure that the line item points to the correct order 

1835 if line_item.order != order: 

1836 raise ValidationError(_('Line item is not associated with this order')) 

1837 

1838 return line_item 

1839 

1840 quantity = serializers.IntegerField( 

1841 min_value=1, required=True, allow_null=False, label=_('Quantity') 

1842 ) 

1843 

1844 serial_numbers = serializers.CharField( 

1845 label=_('Serial Numbers'), 

1846 help_text=_('Enter serial numbers to allocate'), 

1847 required=True, 

1848 allow_blank=False, 

1849 ) 

1850 

1851 shipment = serializers.PrimaryKeyRelatedField( 

1852 queryset=order.models.SalesOrderShipment.objects.all(), 

1853 many=False, 

1854 required=False, 

1855 allow_null=True, 

1856 label=_('Shipment'), 

1857 ) 

1858 

1859 def validate_shipment(self, shipment): 

1860 """Validate the shipment. 

1861 

1862 - Must point to the same order 

1863 - Must not be shipped 

1864 """ 

1865 order = self.context['order'] 

1866 

1867 if shipment and shipment.shipment_date is not None: 

1868 raise ValidationError(_('Shipment has already been shipped')) 

1869 

1870 if shipment and shipment.order != order: 

1871 raise ValidationError(_('Shipment is not associated with this order')) 

1872 

1873 return shipment 

1874 

1875 def validate(self, data): 

1876 """Validation for the serializer. 

1877 

1878 - Ensure the serial_numbers and quantity fields match 

1879 - Check that all serial numbers exist 

1880 - Check that the serial numbers are not yet allocated 

1881 """ 

1882 data = super().validate(data) 

1883 

1884 line_item = data['line_item'] 

1885 quantity = data['quantity'] 

1886 serial_numbers = data['serial_numbers'] 

1887 

1888 part = line_item.part 

1889 

1890 try: 

1891 data['serials'] = extract_serial_numbers( 

1892 serial_numbers, quantity, part.get_latest_serial_number(), part=part 

1893 ) 

1894 except DjangoValidationError as e: 

1895 raise ValidationError({'serial_numbers': e.messages}) 

1896 

1897 serials_not_exist = set() 

1898 serials_unavailable = set() 

1899 stock_items_to_allocate = [] 

1900 

1901 for serial in data['serials']: 

1902 serial = str(serial).strip() 

1903 

1904 items = stock.models.StockItem.objects.filter( 

1905 part=part, serial=serial, quantity=1 

1906 ) 

1907 

1908 if not items.exists(): 

1909 serials_not_exist.add(str(serial)) 

1910 continue 

1911 

1912 stock_item = items[0] 

1913 

1914 if get_global_setting('SALESORDER_BLOCK_INCOMPLETE_ITEM_TESTS'): 

1915 if ( 

1916 stock_item.hasRequiredTests() 

1917 and not stock_item.passedAllRequiredTests() 

1918 ): 

1919 serials_unavailable.add(str(serial)) 

1920 continue 

1921 

1922 if not stock_item.in_stock: 

1923 serials_unavailable.add(str(serial)) 

1924 continue 

1925 

1926 if stock_item.unallocated_quantity() < 1: 

1927 serials_unavailable.add(str(serial)) 

1928 continue 

1929 

1930 # At this point, the serial number is valid, and can be added to the list 

1931 stock_items_to_allocate.append(stock_item) 

1932 

1933 if len(serials_not_exist) > 0: 

1934 error_msg = _('No match found for the following serial numbers') 

1935 error_msg += ': ' 

1936 error_msg += ','.join(sorted(serials_not_exist)) 

1937 

1938 raise ValidationError({'serial_numbers': error_msg}) 

1939 

1940 if len(serials_unavailable) > 0: 

1941 error_msg = _('The following serial numbers are unavailable') 

1942 error_msg += ': ' 

1943 error_msg += ','.join(sorted(serials_unavailable)) 

1944 

1945 raise ValidationError({'serial_numbers': error_msg}) 

1946 

1947 data['stock_items'] = stock_items_to_allocate 

1948 

1949 return data 

1950 

1951 def save(self): 

1952 """Allocate stock items against the sales order.""" 

1953 data = self.validated_data 

1954 

1955 line_item = data['line_item'] 

1956 stock_items = data['stock_items'] 

1957 shipment = data.get('shipment', None) 

1958 

1959 allocations = [] 

1960 

1961 for stock_item in stock_items: 

1962 # Create a new SalesOrderAllocation 

1963 allocations.append( 

1964 order.models.SalesOrderAllocation( 

1965 line=line_item, item=stock_item, quantity=1, shipment=shipment 

1966 ) 

1967 ) 

1968 

1969 with transaction.atomic(): 

1970 order.models.SalesOrderAllocation.objects.bulk_create( 

1971 allocations, batch_size=250 

1972 ) 

1973 

1974 

1975class SalesOrderShipmentAllocationSerializer(serializers.Serializer): 

1976 """DRF serializer for allocation of stock items against a sales order / shipment.""" 

1977 

1978 class Meta: 

1979 """Metaclass options.""" 

1980 

1981 fields = ['items', 'shipment'] 

1982 

1983 items = SalesOrderShipmentAllocationItemSerializer(many=True) 

1984 

1985 shipment = serializers.PrimaryKeyRelatedField( 

1986 queryset=order.models.SalesOrderShipment.objects.all(), 

1987 many=False, 

1988 required=False, 

1989 allow_null=True, 

1990 label=_('Shipment'), 

1991 ) 

1992 

1993 def validate_shipment(self, shipment): 

1994 """Run validation against the provided shipment instance.""" 

1995 order = self.context['order'] 

1996 

1997 if shipment and shipment.shipment_date is not None: 

1998 raise ValidationError(_('Shipment has already been shipped')) 

1999 

2000 if shipment and shipment.order != order: 

2001 raise ValidationError(_('Shipment is not associated with this order')) 

2002 

2003 return shipment 

2004 

2005 def validate(self, data): 

2006 """Serializer validation.""" 

2007 data = super().validate(data) 

2008 

2009 # Extract SalesOrder from serializer context 

2010 # order = self.context['order'] 

2011 

2012 items = data.get('items', []) 

2013 

2014 if len(items) == 0: 2014 ↛ 2017line 2014 didn't jump to line 2017 because the condition on line 2014 was always true

2015 raise ValidationError(_('Allocation items must be provided')) 

2016 

2017 return data 

2018 

2019 def save(self): 

2020 """Perform the allocation of items against this order.""" 

2021 data = self.validated_data 

2022 

2023 items = data['items'] 

2024 shipment = data.get('shipment') 

2025 

2026 with transaction.atomic(): 

2027 for entry in items: 

2028 # Create a new SalesOrderAllocation 

2029 allocation = order.models.SalesOrderAllocation( 

2030 line=entry.get('line_item'), 

2031 item=entry.get('stock_item'), 

2032 quantity=entry.get('quantity'), 

2033 shipment=shipment, 

2034 ) 

2035 

2036 allocation.full_clean() 

2037 allocation.save() 

2038 

2039 

2040class SalesOrderAutoAllocationSerializer(serializers.Serializer): 

2041 """DRF serializer for auto-allocating stock items against a SalesOrder.""" 

2042 

2043 class Meta: 

2044 """Serializer metaclass.""" 

2045 

2046 fields = [ 

2047 'location', 

2048 'exclude_location', 

2049 'shipment', 

2050 'interchangeable', 

2051 'stock_sort_by', 

2052 'serialized_stock', 

2053 'line_items', 

2054 ] 

2055 

2056 location = serializers.PrimaryKeyRelatedField( 

2057 queryset=stock.models.StockLocation.objects.all(), 

2058 many=False, 

2059 allow_null=True, 

2060 required=False, 

2061 label=_('Source Location'), 

2062 help_text=_( 

2063 'Stock location where items are sourced (leave blank to use any location)' 

2064 ), 

2065 ) 

2066 

2067 exclude_location = serializers.PrimaryKeyRelatedField( 

2068 queryset=stock.models.StockLocation.objects.all(), 

2069 many=False, 

2070 allow_null=True, 

2071 required=False, 

2072 label=_('Exclude Location'), 

2073 help_text=_('Exclude stock items from this location'), 

2074 ) 

2075 

2076 shipment = serializers.PrimaryKeyRelatedField( 

2077 queryset=order.models.SalesOrderShipment.objects.all(), 

2078 many=False, 

2079 allow_null=True, 

2080 required=False, 

2081 label=_('Shipment'), 

2082 help_text=_('Assign allocations to this shipment'), 

2083 ) 

2084 

2085 interchangeable = serializers.BooleanField( 

2086 default=True, 

2087 label=_('Interchangeable Stock'), 

2088 help_text=_( 

2089 'Allow stock to be taken from multiple locations to fulfil a single line item' 

2090 ), 

2091 ) 

2092 

2093 stock_sort_by = serializers.ChoiceField( 

2094 default=stock.models.STOCK_SORT_DEFAULT, 

2095 choices=stock.models.STOCK_SORT_CHOICES, 

2096 label=_('Stock Priority'), 

2097 help_text=_('Preferred order in which matching stock items are consumed'), 

2098 ) 

2099 

2100 serialized_stock = serializers.ChoiceField( 

2101 default=order.models.SERIALIZED_STOCK_DEFAULT, 

2102 choices=order.models.SERIALIZED_STOCK_CHOICES, 

2103 label=_('Serialized Stock'), 

2104 help_text=_( 

2105 'Control whether serialized stock items are included in auto-allocation' 

2106 ), 

2107 ) 

2108 

2109 line_items = serializers.PrimaryKeyRelatedField( 

2110 queryset=order.models.SalesOrderLineItem.objects.all(), 

2111 many=True, 

2112 required=False, 

2113 default=list, 

2114 label=_('Line Items'), 

2115 help_text=_( 

2116 'Limit allocation to these line items (leave blank to allocate all lines)' 

2117 ), 

2118 ) 

2119 

2120 def validate_shipment(self, shipment): 

2121 """Validate that the shipment belongs to this order and is not yet shipped.""" 

2122 order_obj = self.context.get('order') 

2123 

2124 if shipment is None: 

2125 return shipment 

2126 

2127 if shipment.shipment_date is not None: 

2128 raise ValidationError(_('Shipment has already been shipped')) 

2129 

2130 if order_obj and shipment.order != order_obj: 

2131 raise ValidationError(_('Shipment is not associated with this order')) 

2132 

2133 return shipment 

2134 

2135 def validate_line_items(self, line_items): 

2136 """Validate that all provided line items belong to this order.""" 

2137 order_obj = self.context.get('order') 

2138 

2139 if order_obj and line_items: 

2140 for line in line_items: 

2141 if line.order != order_obj: 

2142 raise ValidationError(_('Line item does not belong to this order')) 

2143 

2144 return line_items 

2145 

2146 

2147@register_importer() 

2148class SalesOrderExtraLineSerializer( 

2149 AbstractExtraLineSerializer, InvenTreeModelSerializer 

2150): 

2151 """Serializer for a SalesOrderExtraLine object.""" 

2152 

2153 class Meta(AbstractExtraLineMeta): 

2154 """Metaclass options.""" 

2155 

2156 model = order.models.SalesOrderExtraLine 

2157 fields = AbstractExtraLineSerializer.extra_line_fields([]) 

2158 

2159 order_detail = OptionalField( 

2160 serializer_class=SalesOrderSerializer, 

2161 serializer_kwargs={ 

2162 'source': 'order', 

2163 'many': False, 

2164 'read_only': True, 

2165 'allow_null': True, 

2166 }, 

2167 ) 

2168 

2169 

2170@register_importer() 

2171class ReturnOrderSerializer( 

2172 NotesFieldMixin, 

2173 InvenTreeCustomStatusSerializerMixin, 

2174 AbstractOrderSerializer, 

2175 TotalPriceMixin, 

2176 InvenTreeModelSerializer, 

2177): 

2178 """Serializer for the ReturnOrder model class.""" 

2179 

2180 class Meta: 

2181 """Metaclass options.""" 

2182 

2183 model = order.models.ReturnOrder 

2184 fields = AbstractOrderSerializer.order_fields([ 

2185 'complete_date', 

2186 'customer', 

2187 'customer_detail', 

2188 'customer_reference', 

2189 'order_currency', 

2190 'total_price', 

2191 'updated_at', 

2192 ]) 

2193 read_only_fields = ['creation_date', 'updated_at'] 

2194 

2195 def skip_create_fields(self): 

2196 """Skip these fields when instantiating a new object.""" 

2197 fields = super().skip_create_fields() 

2198 

2199 return [*fields, 'duplicate'] 

2200 

2201 @staticmethod 

2202 def annotate_queryset(queryset): 

2203 """Custom annotation for the serializer queryset.""" 

2204 queryset = AbstractOrderSerializer.annotate_queryset(queryset) 

2205 

2206 queryset = queryset.annotate( 

2207 completed_lines=SubqueryCount( 

2208 'lines', filter=~Q(outcome=ReturnOrderLineStatus.PENDING.value) 

2209 ) 

2210 ) 

2211 

2212 queryset = queryset.annotate( 

2213 overdue=Case( 

2214 When( 

2215 order.models.ReturnOrder.overdue_filter(), 

2216 then=Value(True, output_field=BooleanField()), 

2217 ), 

2218 default=Value(False, output_field=BooleanField()), 

2219 ) 

2220 ) 

2221 

2222 return queryset 

2223 

2224 customer_detail = OptionalField( 

2225 serializer_class=CompanyBriefSerializer, 

2226 serializer_kwargs={ 

2227 'source': 'customer', 

2228 'many': False, 

2229 'read_only': True, 

2230 'allow_null': True, 

2231 }, 

2232 prefetch_fields=['customer'], 

2233 ) 

2234 

2235 

2236class ReturnOrderHoldSerializer(OrderAdjustSerializer): 

2237 """Serializers for holding a ReturnOrder.""" 

2238 

2239 def save(self): 

2240 """Save the serializer to 'hold' the order.""" 

2241 self.order.hold_order() 

2242 

2243 

2244class ReturnOrderIssueSerializer(OrderAdjustSerializer): 

2245 """Serializer for issuing a ReturnOrder.""" 

2246 

2247 def save(self): 

2248 """Save the serializer to 'issue' the order.""" 

2249 self.order.issue_order() 

2250 

2251 

2252class ReturnOrderCancelSerializer(OrderAdjustSerializer): 

2253 """Serializer for cancelling a ReturnOrder.""" 

2254 

2255 def save(self): 

2256 """Save the serializer to 'cancel' the order.""" 

2257 self.order.cancel_order() 

2258 

2259 

2260class ReturnOrderCompleteSerializer(OrderAdjustSerializer): 

2261 """Serializer for completing a ReturnOrder.""" 

2262 

2263 def save(self): 

2264 """Save the serializer to 'complete' the order.""" 

2265 self.order.complete_order() 

2266 

2267 

2268class ReturnOrderLineItemReceiveSerializer(serializers.Serializer): 

2269 """Serializer for receiving a single line item against a ReturnOrder.""" 

2270 

2271 class Meta: 

2272 """Metaclass options.""" 

2273 

2274 fields = ['item', 'status'] 

2275 

2276 item = serializers.PrimaryKeyRelatedField( 

2277 queryset=order.models.ReturnOrderLineItem.objects.all(), 

2278 many=False, 

2279 allow_null=False, 

2280 required=True, 

2281 label=_('Return order line item'), 

2282 ) 

2283 

2284 status = stock.serializers.StockStatusCustomSerializer( 

2285 default=None, required=False, allow_blank=True 

2286 ) 

2287 

2288 def validate_line_item(self, item): 

2289 """Validation for a single line item.""" 

2290 if item.order != self.context['order']: 

2291 raise ValidationError(_('Line item does not match return order')) 

2292 

2293 if item.received: 

2294 raise ValidationError(_('Line item has already been received')) 

2295 

2296 return item 

2297 

2298 

2299class ReturnOrderReceiveSerializer(serializers.Serializer): 

2300 """Serializer for receiving items against a ReturnOrder.""" 

2301 

2302 class Meta: 

2303 """Metaclass options.""" 

2304 

2305 fields = ['items', 'location', 'note'] 

2306 

2307 items = ReturnOrderLineItemReceiveSerializer(many=True) 

2308 

2309 location = serializers.PrimaryKeyRelatedField( 

2310 queryset=stock.models.StockLocation.objects.all(), 

2311 many=False, 

2312 allow_null=False, 

2313 required=True, 

2314 label=_('Location'), 

2315 help_text=_('Select destination location for received items'), 

2316 ) 

2317 

2318 note = serializers.CharField( 

2319 label=_('Note'), 

2320 help_text=_('Additional note for incoming stock items'), 

2321 required=False, 

2322 default='', 

2323 allow_blank=True, 

2324 ) 

2325 

2326 def validate(self, data): 

2327 """Perform data validation for this serializer.""" 

2328 order = self.context['order'] 

2329 if order.status != ReturnOrderStatus.IN_PROGRESS: 

2330 raise ValidationError( 

2331 _('Items can only be received against orders which are in progress') 

2332 ) 

2333 

2334 data = super().validate(data) 

2335 

2336 items = data.get('items', []) 

2337 

2338 if len(items) == 0: 

2339 raise ValidationError(_('Line items must be provided')) 

2340 

2341 return data 

2342 

2343 @transaction.atomic 

2344 def save(self): 

2345 """Saving this serializer marks the returned items as received.""" 

2346 order = self.context['order'] 

2347 request = self.context.get('request') 

2348 

2349 data = self.validated_data 

2350 items = data['items'] 

2351 location = data['location'] 

2352 

2353 with transaction.atomic(): 

2354 for item in items: 

2355 line_item = item['item'] 

2356 

2357 order.receive_line_item( 

2358 line_item, 

2359 location, 

2360 request.user if request else None, 

2361 note=data.get('note', ''), 

2362 status=item.get('status', None), 

2363 ) 

2364 

2365 

2366@register_importer() 

2367class ReturnOrderLineItemSerializer( 

2368 DataImportExportSerializerMixin, 

2369 AbstractLineItemSerializer, 

2370 InvenTreeModelSerializer, 

2371): 

2372 """Serializer for a ReturnOrderLineItem object.""" 

2373 

2374 class Meta: 

2375 """Metaclass options.""" 

2376 

2377 model = order.models.ReturnOrderLineItem 

2378 fields = AbstractLineItemSerializer.line_fields([ 

2379 'item', 

2380 'received_date', 

2381 'outcome', 

2382 'price', 

2383 'price_currency', 

2384 # Filterable detail fields 

2385 'item_detail', 

2386 'part_detail', 

2387 ]) 

2388 

2389 order_detail = OptionalField( 

2390 serializer_class=ReturnOrderSerializer, 

2391 serializer_kwargs={ 

2392 'source': 'order', 

2393 'many': False, 

2394 'read_only': True, 

2395 'allow_null': True, 

2396 }, 

2397 prefetch_fields=[ 

2398 'order__created_by', 

2399 'order__responsible', 

2400 'order__address', 

2401 'order__project_code', 

2402 'order__contact', 

2403 ], 

2404 ) 

2405 

2406 quantity = serializers.FloatField( 

2407 label=_('Quantity'), help_text=_('Quantity to return') 

2408 ) 

2409 

2410 item_detail = OptionalField( 

2411 serializer_class=stock.serializers.StockItemSerializer, 

2412 serializer_kwargs={ 

2413 'source': 'item', 

2414 'many': False, 

2415 'read_only': True, 

2416 'allow_null': True, 

2417 }, 

2418 prefetch_fields=['item__supplier_part'], 

2419 ) 

2420 

2421 part_detail = OptionalField( 

2422 serializer_class=PartBriefSerializer, 

2423 serializer_kwargs={ 

2424 'source': 'item.part', 

2425 'many': False, 

2426 'read_only': True, 

2427 'allow_null': True, 

2428 }, 

2429 ) 

2430 

2431 price = InvenTreeMoneySerializer(allow_null=True) 

2432 price_currency = InvenTreeCurrencySerializer(help_text=_('Line price currency')) 

2433 

2434 

2435@register_importer() 

2436class ReturnOrderExtraLineSerializer( 

2437 AbstractExtraLineSerializer, InvenTreeModelSerializer 

2438): 

2439 """Serializer for a ReturnOrderExtraLine object.""" 

2440 

2441 class Meta(AbstractExtraLineMeta): 

2442 """Metaclass options.""" 

2443 

2444 model = order.models.ReturnOrderExtraLine 

2445 fields = AbstractExtraLineSerializer.extra_line_fields([]) 

2446 

2447 order_detail = OptionalField( 

2448 serializer_class=ReturnOrderSerializer, 

2449 serializer_kwargs={ 

2450 'source': 'order', 

2451 'many': False, 

2452 'read_only': True, 

2453 'allow_null': True, 

2454 }, 

2455 ) 

2456 

2457 

2458@register_importer() 

2459class TransferOrderSerializer( 

2460 NotesFieldMixin, 

2461 InvenTreeCustomStatusSerializerMixin, 

2462 AbstractOrderSerializer, 

2463 InvenTreeModelSerializer, 

2464): 

2465 """Serializer for a TransferOrder object.""" 

2466 

2467 class Meta: 

2468 """Metaclass options.""" 

2469 

2470 model = order.models.TransferOrder 

2471 fields = AbstractOrderSerializer.order_fields([ 

2472 'take_from', 

2473 'take_from_detail', 

2474 'destination', 

2475 'destination_detail', 

2476 'consume', 

2477 'complete_date', 

2478 ]) 

2479 read_only_fields = ['creation_date'] 

2480 extra_kwargs = {} 

2481 

2482 def skip_create_fields(self): 

2483 """Skip these fields when instantiating a new object.""" 

2484 fields = super().skip_create_fields() 

2485 

2486 return [*fields, 'duplicate'] 

2487 

2488 @staticmethod 

2489 def annotate_queryset(queryset): 

2490 """Add extra information to the queryset. 

2491 

2492 - Number of line items in the TransferOrder 

2493 - Number of completed line items in the TransferOrder 

2494 - Overdue status of the TransferOrder 

2495 """ 

2496 queryset = AbstractOrderSerializer.annotate_queryset(queryset) 

2497 

2498 queryset = queryset.annotate( 

2499 completed_lines=SubqueryCount( 

2500 'lines', filter=Q(quantity__lte=F('transferred')) 

2501 ) 

2502 ) 

2503 

2504 queryset = queryset.annotate( 

2505 overdue=Case( 

2506 When( 

2507 order.models.TransferOrder.overdue_filter(), 

2508 then=Value(True, output_field=BooleanField()), 

2509 ), 

2510 default=Value(False, output_field=BooleanField()), 

2511 ) 

2512 ) 

2513 

2514 return queryset 

2515 

2516 take_from_detail = OptionalField( 

2517 serializer_class=stock.serializers.LocationSerializer, 

2518 serializer_kwargs={ 

2519 'source': 'take_from', 

2520 'many': False, 

2521 'read_only': True, 

2522 'allow_null': True, 

2523 }, 

2524 default_include=True, 

2525 ) 

2526 

2527 destination_detail = OptionalField( 

2528 serializer_class=stock.serializers.LocationSerializer, 

2529 serializer_kwargs={ 

2530 'source': 'destination', 

2531 'many': False, 

2532 'read_only': True, 

2533 'allow_null': True, 

2534 }, 

2535 default_include=True, 

2536 ) 

2537 

2538 

2539class TransferOrderHoldSerializer(OrderAdjustSerializer): 

2540 """Serializer for placing a TransferOrder on hold.""" 

2541 

2542 def save(self): 

2543 """Save the serializer to 'hold' the order.""" 

2544 self.order.hold_order() 

2545 

2546 

2547class TransferOrderIssueSerializer(OrderAdjustSerializer): 

2548 """Serializer for issuing a transfer order.""" 

2549 

2550 def save(self): 

2551 """Save the serializer to 'issue' the order.""" 

2552 self.order.issue_order() 

2553 

2554 

2555class TransferOrderCancelSerializer(OrderAdjustSerializer): 

2556 """Serializer for cancelling a TransferOrder.""" 

2557 

2558 def save(self): 

2559 """Save the serializer to 'cancel' the order.""" 

2560 if not self.order.can_cancel: 2560 ↛ 2561line 2560 didn't jump to line 2561 because the condition on line 2560 was never true

2561 raise ValidationError(_('Order cannot be cancelled')) 

2562 

2563 self.order.cancel_order() 

2564 

2565 

2566class TransferOrderCompleteSerializer(OrderAdjustSerializer): 

2567 """Serializer for completing a transfer order.""" 

2568 

2569 class Meta: 

2570 """Metaclass options.""" 

2571 

2572 fields = ['accept_incomplete_allocation'] 

2573 

2574 accept_incomplete_allocation = serializers.BooleanField( 

2575 label=_('Accept Incomplete Allocation'), 

2576 help_text=_('Allow order to complete with incomplete allocations'), 

2577 required=False, 

2578 default=False, 

2579 ) 

2580 

2581 def validate_accept_incomplete_allocation(self, value): 

2582 """Check if the 'accept_incomplete_allocation' field is required.""" 

2583 order = self.context['order'] 

2584 

2585 if not value and not order.is_fully_allocated(): 2585 ↛ 2586line 2585 didn't jump to line 2586 because the condition on line 2585 was never true

2586 raise ValidationError(_('Order has incomplete allocations')) 

2587 

2588 return value 

2589 

2590 def get_context_data(self): 

2591 """Custom context information for this serializer.""" 

2592 order = self.context['order'] 

2593 

2594 return {'is_complete': order.is_completed()} 

2595 

2596 def validate(self, data): 

2597 """Custom validation for the serializer.""" 

2598 data = super().validate(data) 

2599 self.order.can_complete( 

2600 raise_error=True, 

2601 allow_incomplete_lines=str2bool( 

2602 data.get('accept_incomplete_allocation', False) 

2603 ), 

2604 ) 

2605 return data 

2606 

2607 def save(self): 

2608 """Save the serializer to 'complete' the order.""" 

2609 request = self.context.get('request') 

2610 data = self.validated_data 

2611 user = request.user if request else None 

2612 

2613 self.order.complete_order( 

2614 user=user, 

2615 allow_incomplete_lines=data.get('accept_incomplete_allocation', False), 

2616 ) 

2617 

2618 

2619@register_importer() 

2620class TransferOrderLineItemSerializer( 

2621 DataImportExportSerializerMixin, 

2622 AbstractLineItemSerializer, 

2623 InvenTreeModelSerializer, 

2624): 

2625 """Serializer for a TransferOrderLineItem object.""" 

2626 

2627 class Meta: 

2628 """Metaclass options.""" 

2629 

2630 model = order.models.TransferOrderLineItem 

2631 fields = AbstractLineItemSerializer.line_fields([ 

2632 'allocated', 

2633 'overdue', 

2634 'part', 

2635 'part_detail', 

2636 'transferred', 

2637 # Annotated fields for part stocking information 

2638 'available_stock', 

2639 'available_variant_stock', 

2640 'building', 

2641 'on_order', 

2642 # Filterable detail fields 

2643 ]) 

2644 

2645 @staticmethod 

2646 def annotate_queryset(queryset): 

2647 """Add some extra annotations to this queryset. 

2648 

2649 - "overdue" status (boolean field) 

2650 - "available_quantity" 

2651 - "building" 

2652 - "on_order" 

2653 """ 

2654 queryset = queryset.annotate( 

2655 overdue=Case( 

2656 When( 

2657 Q(order__status__in=TransferOrderStatusGroups.OPEN) 

2658 & order.models.TransferOrderLineItem.OVERDUE_FILTER, 

2659 then=Value(True, output_field=BooleanField()), 

2660 ), 

2661 default=Value(False, output_field=BooleanField()), 

2662 ) 

2663 ) 

2664 

2665 # Annotate each line with the available stock quantity 

2666 # To do this, we need to look at the total stock and any allocations 

2667 queryset = queryset.alias( 

2668 total_stock=part_filters.annotate_total_stock(reference='part__'), 

2669 allocated_to_sales_orders=part_filters.annotate_sales_order_allocations( 

2670 reference='part__' 

2671 ), 

2672 allocated_to_build_orders=part_filters.annotate_build_order_allocations( 

2673 reference='part__' 

2674 ), 

2675 ) 

2676 

2677 queryset = queryset.annotate( 

2678 available_stock=Greatest( 

2679 ExpressionWrapper( 

2680 F('total_stock') 

2681 - F('allocated_to_sales_orders') 

2682 - F('allocated_to_build_orders'), 

2683 output_field=models.DecimalField(), 

2684 ), 

2685 0, 

2686 output_field=models.DecimalField(), 

2687 ) 

2688 ) 

2689 

2690 # Add information about the quantity of parts currently on order 

2691 queryset = queryset.annotate( 

2692 on_order=part_filters.annotate_on_order_quantity(reference='part__') 

2693 ) 

2694 

2695 # Add information about the quantity of parts currently in production 

2696 queryset = queryset.annotate( 

2697 building=part_filters.annotate_in_production_quantity(reference='part__') 

2698 ) 

2699 

2700 # Annotate total 'allocated' stock quantity 

2701 queryset = queryset.annotate( 

2702 allocated=Coalesce( 

2703 SubquerySum('allocations__quantity'), 

2704 Decimal(0), 

2705 output_field=models.DecimalField(), 

2706 ) 

2707 ) 

2708 

2709 return queryset 

2710 

2711 order_detail = OptionalField( 

2712 serializer_class=TransferOrderSerializer, 

2713 serializer_kwargs={ 

2714 'source': 'order', 

2715 'many': False, 

2716 'read_only': True, 

2717 'allow_null': True, 

2718 }, 

2719 prefetch_fields=[ 

2720 'order__created_by', 

2721 'order__responsible', 

2722 'order__project_code', 

2723 ], 

2724 ) 

2725 

2726 part_detail = OptionalField( 

2727 serializer_class=PartBriefSerializer, 

2728 serializer_kwargs={ 

2729 'source': 'part', 

2730 'many': False, 

2731 'read_only': True, 

2732 'allow_null': True, 

2733 }, 

2734 prefetch_fields=['part__pricing_data'], 

2735 ) 

2736 

2737 # Annotated fields 

2738 overdue = serializers.BooleanField(read_only=True, allow_null=True) 

2739 available_stock = serializers.FloatField(read_only=True) 

2740 available_variant_stock = serializers.FloatField(read_only=True) 

2741 on_order = serializers.FloatField(label=_('On Order'), read_only=True) 

2742 building = serializers.FloatField(label=_('In Production'), read_only=True) 

2743 

2744 quantity = InvenTreeDecimalField() 

2745 

2746 allocated = serializers.FloatField(read_only=True) 

2747 

2748 transferred = InvenTreeDecimalField(read_only=True) 

2749 

2750 

2751class TransferOrderAllocationItemSerializer(serializers.Serializer): 

2752 """A serializer for allocating a single stock-item against a TransferOrder line item.""" 

2753 

2754 class Meta: 

2755 """Metaclass options.""" 

2756 

2757 fields = ['line_item', 'stock_item', 'quantity'] 

2758 

2759 line_item = serializers.PrimaryKeyRelatedField( 

2760 queryset=order.models.TransferOrderLineItem.objects.all(), 

2761 many=False, 

2762 allow_null=False, 

2763 required=True, 

2764 label=_('Stock Item'), 

2765 ) 

2766 

2767 def validate_line_item(self, line_item): 

2768 """Custom validation for the 'line_item' field. 

2769 

2770 - Ensure the line_item is associated with the particular TransferOrder 

2771 """ 

2772 order = self.context['order'] 

2773 

2774 # Ensure that the line item points to the correct order 

2775 if line_item.order != order: 

2776 raise ValidationError(_('Line item is not associated with this order')) 

2777 

2778 return line_item 

2779 

2780 stock_item = serializers.PrimaryKeyRelatedField( 

2781 queryset=stock.models.StockItem.objects.all(), 

2782 many=False, 

2783 allow_null=False, 

2784 required=True, 

2785 label=_('Stock Item'), 

2786 ) 

2787 

2788 quantity = serializers.DecimalField( 

2789 max_digits=15, decimal_places=5, min_value=Decimal(0), required=True 

2790 ) 

2791 

2792 def validate_quantity(self, quantity): 

2793 """Custom validation for the 'quantity' field.""" 

2794 if quantity <= 0: 2794 ↛ 2797line 2794 didn't jump to line 2797 because the condition on line 2794 was always true

2795 raise ValidationError(_('Quantity must be positive')) 

2796 

2797 return quantity 

2798 

2799 def validate(self, data): 

2800 """Custom validation for the serializer. 

2801 

2802 - Ensure that the quantity is 1 for serialized stock 

2803 - Quantity cannot exceed the available amount 

2804 """ 

2805 data = super().validate(data) 

2806 

2807 stock_item = data['stock_item'] 

2808 quantity = data['quantity'] 

2809 

2810 if stock_item.serialized and quantity != 1: 

2811 raise ValidationError({ 

2812 'quantity': _('Quantity must be 1 for serialized stock item') 

2813 }) 

2814 

2815 q = normalize(stock_item.unallocated_quantity()) 

2816 

2817 if quantity > q: 

2818 raise ValidationError({'quantity': _(f'Available quantity ({q}) exceeded')}) 

2819 

2820 return data 

2821 

2822 

2823class TransferOrderLineItemAllocationSerializer(serializers.Serializer): 

2824 """DRF serializer for allocation of stock items against a transfer order line item.""" 

2825 

2826 class Meta: 

2827 """Metaclass options.""" 

2828 

2829 fields = ['items'] 

2830 

2831 items = TransferOrderAllocationItemSerializer(many=True) 

2832 

2833 def validate(self, data): 

2834 """Serializer validation.""" 

2835 data = super().validate(data) 

2836 

2837 # Extract TransferOrder from serializer context 

2838 # order = self.context['order'] 

2839 

2840 items = data.get('items', []) 

2841 

2842 if len(items) == 0: 2842 ↛ 2845line 2842 didn't jump to line 2845 because the condition on line 2842 was always true

2843 raise ValidationError(_('Allocation items must be provided')) 

2844 

2845 return data 

2846 

2847 def save(self): 

2848 """Perform the allocation of items against this order.""" 

2849 data = self.validated_data 

2850 

2851 items = data['items'] 

2852 

2853 with transaction.atomic(): 

2854 for entry in items: 

2855 # Create a new TransferOrderAllocation 

2856 allocation = order.models.TransferOrderAllocation( 

2857 line=entry.get('line_item'), 

2858 item=entry.get('stock_item'), 

2859 quantity=entry.get('quantity'), 

2860 ) 

2861 

2862 allocation.full_clean() 

2863 allocation.save() 

2864 

2865 

2866class TransferOrderAllocationSerializer( 

2867 FilterableSerializerMixin, InvenTreeModelSerializer 

2868): 

2869 """Serializer for the TransferOrderAllocation model. 

2870 

2871 This includes some fields from the related model objects. 

2872 """ 

2873 

2874 class Meta: 

2875 """Metaclass options.""" 

2876 

2877 model = order.models.TransferOrderAllocation 

2878 fields = [ 

2879 'pk', 

2880 'item', 

2881 'quantity', 

2882 # Annotated read-only fields 

2883 'line', 

2884 'part', 

2885 'order', 

2886 'serial', 

2887 'location', 

2888 # Extra detail fields 

2889 'item_detail', 

2890 'part_detail', 

2891 'order_detail', 

2892 'location_detail', 

2893 ] 

2894 read_only_fields = ['line', ''] 

2895 

2896 part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) 

2897 order = serializers.PrimaryKeyRelatedField( 

2898 source='line.order', many=False, read_only=True 

2899 ) 

2900 serial = serializers.CharField(source='get_serial', read_only=True, allow_null=True) 

2901 quantity = serializers.FloatField(read_only=False) 

2902 location = serializers.PrimaryKeyRelatedField( 

2903 source='item.location', many=False, read_only=True 

2904 ) 

2905 

2906 # Extra detail fields 

2907 order_detail = OptionalField( 

2908 serializer_class=TransferOrderSerializer, 

2909 serializer_kwargs={ 

2910 'source': 'line.order', 

2911 'many': False, 

2912 'read_only': True, 

2913 'allow_null': True, 

2914 }, 

2915 ) 

2916 

2917 part_detail = OptionalField( 

2918 serializer_class=PartBriefSerializer, 

2919 serializer_kwargs={ 

2920 'source': 'item.part', 

2921 'many': False, 

2922 'read_only': True, 

2923 'allow_null': True, 

2924 }, 

2925 ) 

2926 

2927 item_detail = OptionalField( 

2928 serializer_class=stock.serializers.StockItemSerializer, 

2929 serializer_kwargs={ 

2930 'source': 'item', 

2931 'many': False, 

2932 'read_only': True, 

2933 'allow_null': True, 

2934 'part_detail': False, 

2935 'location_detail': False, 

2936 'supplier_part_detail': False, 

2937 }, 

2938 ) 

2939 

2940 location_detail = OptionalField( 

2941 serializer_class=stock.serializers.LocationBriefSerializer, 

2942 serializer_kwargs={ 

2943 'source': 'item.location', 

2944 'many': False, 

2945 'read_only': True, 

2946 'allow_null': True, 

2947 }, 

2948 ) 

2949 

2950 

2951class TransferOrderSerialAllocationSerializer(serializers.Serializer): 

2952 """DRF serializer for allocation of serial numbers against a transfer order.""" 

2953 

2954 class Meta: 

2955 """Metaclass options.""" 

2956 

2957 fields = ['line_item', 'quantity', 'serial_numbers'] 

2958 

2959 line_item = serializers.PrimaryKeyRelatedField( 

2960 queryset=order.models.TransferOrderLineItem.objects.all(), 

2961 many=False, 

2962 required=True, 

2963 allow_null=False, 

2964 label=_('Line Item'), 

2965 ) 

2966 

2967 def validate_line_item(self, line_item): 

2968 """Ensure that the line_item is valid.""" 

2969 order = self.context['order'] 

2970 

2971 # Ensure that the line item points to the correct order 

2972 if line_item.order != order: 

2973 raise ValidationError(_('Line item is not associated with this order')) 

2974 

2975 return line_item 

2976 

2977 quantity = serializers.IntegerField( 

2978 min_value=1, required=True, allow_null=False, label=_('Quantity') 

2979 ) 

2980 

2981 serial_numbers = serializers.CharField( 

2982 label=_('Serial Numbers'), 

2983 help_text=_('Enter serial numbers to allocate'), 

2984 required=True, 

2985 allow_blank=False, 

2986 ) 

2987 

2988 def validate(self, data): 

2989 """Validation for the serializer. 

2990 

2991 - Ensure the serial_numbers and quantity fields match 

2992 - Check that all serial numbers exist 

2993 - Check that the serial numbers are not yet allocated 

2994 """ 

2995 data = super().validate(data) 

2996 

2997 line_item = data['line_item'] 

2998 quantity = data['quantity'] 

2999 serial_numbers = data['serial_numbers'] 

3000 

3001 part = line_item.part 

3002 

3003 try: 

3004 data['serials'] = extract_serial_numbers( 

3005 serial_numbers, quantity, part.get_latest_serial_number(), part=part 

3006 ) 

3007 except DjangoValidationError as e: 

3008 raise ValidationError({'serial_numbers': e.messages}) 

3009 

3010 serials_not_exist = set() 

3011 serials_unavailable = set() 

3012 stock_items_to_allocate = [] 

3013 

3014 for serial in data['serials']: 

3015 serial = str(serial).strip() 

3016 

3017 items = stock.models.StockItem.objects.filter( 

3018 part=part, serial=serial, quantity=1 

3019 ) 

3020 

3021 if not items.exists(): 

3022 serials_not_exist.add(str(serial)) 

3023 continue 

3024 

3025 stock_item = items[0] 

3026 

3027 if not stock_item.in_stock: 

3028 serials_unavailable.add(str(serial)) 

3029 continue 

3030 

3031 if stock_item.unallocated_quantity() < 1: 

3032 serials_unavailable.add(str(serial)) 

3033 continue 

3034 

3035 # At this point, the serial number is valid, and can be added to the list 

3036 stock_items_to_allocate.append(stock_item) 

3037 

3038 if len(serials_not_exist) > 0: 

3039 error_msg = _('No match found for the following serial numbers') 

3040 error_msg += ': ' 

3041 error_msg += ','.join(sorted(serials_not_exist)) 

3042 

3043 raise ValidationError({'serial_numbers': error_msg}) 

3044 

3045 if len(serials_unavailable) > 0: 

3046 error_msg = _('The following serial numbers are unavailable') 

3047 error_msg += ': ' 

3048 error_msg += ','.join(sorted(serials_unavailable)) 

3049 

3050 raise ValidationError({'serial_numbers': error_msg}) 

3051 

3052 data['stock_items'] = stock_items_to_allocate 

3053 

3054 return data 

3055 

3056 def save(self): 

3057 """Allocate stock items against the transfer order.""" 

3058 data = self.validated_data 

3059 

3060 line_item = data['line_item'] 

3061 stock_items = data['stock_items'] 

3062 

3063 allocations = [] 

3064 

3065 for stock_item in stock_items: 

3066 # Create a new TransferOrderAllocation 

3067 allocations.append( 

3068 order.models.TransferOrderAllocation( 

3069 line=line_item, item=stock_item, quantity=1 

3070 ) 

3071 ) 

3072 

3073 with transaction.atomic(): 

3074 order.models.TransferOrderAllocation.objects.bulk_create(allocations)