mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Merge branch 'inventree:master' into matmair/issue2788
This commit is contained in:
@ -8,11 +8,35 @@ from import_export.admin import ImportExportModelAdmin
|
||||
from import_export.resources import ModelResource
|
||||
from import_export.fields import Field
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
from .models import SalesOrder, SalesOrderLineItem
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine
|
||||
from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine
|
||||
from .models import SalesOrderShipment, SalesOrderAllocation
|
||||
|
||||
|
||||
# region general classes
|
||||
class GeneralExtraLineAdmin:
|
||||
list_display = (
|
||||
'order',
|
||||
'quantity',
|
||||
'reference'
|
||||
)
|
||||
|
||||
search_fields = [
|
||||
'order__reference',
|
||||
'order__customer__name',
|
||||
'reference',
|
||||
]
|
||||
|
||||
autocomplete_fields = ('order', )
|
||||
|
||||
|
||||
class GeneralExtraLineMeta:
|
||||
skip_unchanged = True
|
||||
report_skipped = False
|
||||
clean_model_instances = True
|
||||
# endregion
|
||||
|
||||
|
||||
class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
|
||||
model = PurchaseOrderLineItem
|
||||
extra = 0
|
||||
@ -68,8 +92,8 @@ class SalesOrderAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('customer',)
|
||||
|
||||
|
||||
class POLineItemResource(ModelResource):
|
||||
""" Class for managing import / export of POLineItem data """
|
||||
class PurchaseOrderLineItemResource(ModelResource):
|
||||
""" Class for managing import / export of PurchaseOrderLineItem data """
|
||||
|
||||
part_name = Field(attribute='part__part__name', readonly=True)
|
||||
|
||||
@ -86,9 +110,16 @@ class POLineItemResource(ModelResource):
|
||||
clean_model_instances = True
|
||||
|
||||
|
||||
class SOLineItemResource(ModelResource):
|
||||
class PurchaseOrderExtraLineResource(ModelResource):
|
||||
""" Class for managing import / export of PurchaseOrderExtraLine data """
|
||||
|
||||
class Meta(GeneralExtraLineMeta):
|
||||
model = PurchaseOrderExtraLine
|
||||
|
||||
|
||||
class SalesOrderLineItemResource(ModelResource):
|
||||
"""
|
||||
Class for managing import / export of SOLineItem data
|
||||
Class for managing import / export of SalesOrderLineItem data
|
||||
"""
|
||||
|
||||
part_name = Field(attribute='part__name', readonly=True)
|
||||
@ -117,9 +148,16 @@ class SOLineItemResource(ModelResource):
|
||||
clean_model_instances = True
|
||||
|
||||
|
||||
class SalesOrderExtraLineResource(ModelResource):
|
||||
""" Class for managing import / export of SalesOrderExtraLine data """
|
||||
|
||||
class Meta(GeneralExtraLineMeta):
|
||||
model = SalesOrderExtraLine
|
||||
|
||||
|
||||
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
resource_class = POLineItemResource
|
||||
resource_class = PurchaseOrderLineItemResource
|
||||
|
||||
list_display = (
|
||||
'order',
|
||||
@ -133,9 +171,14 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('order', 'part', 'destination',)
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
|
||||
resource_class = PurchaseOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
resource_class = SOLineItemResource
|
||||
resource_class = SalesOrderLineItemResource
|
||||
|
||||
list_display = (
|
||||
'order',
|
||||
@ -154,6 +197,11 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
autocomplete_fields = ('order', 'part',)
|
||||
|
||||
|
||||
class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
|
||||
resource_class = SalesOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderShipmentAdmin(ImportExportModelAdmin):
|
||||
|
||||
list_display = [
|
||||
@ -184,9 +232,11 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin):
|
||||
|
||||
admin.site.register(PurchaseOrder, PurchaseOrderAdmin)
|
||||
admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin)
|
||||
admin.site.register(PurchaseOrderExtraLine, PurchaseOrderExtraLineAdmin)
|
||||
|
||||
admin.site.register(SalesOrder, SalesOrderAdmin)
|
||||
admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin)
|
||||
admin.site.register(SalesOrderExtraLine, SalesOrderExtraLineAdmin)
|
||||
|
||||
admin.site.register(SalesOrderShipment, SalesOrderShipmentAdmin)
|
||||
admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin)
|
||||
|
@ -5,7 +5,7 @@ JSON API for the Order app
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import include, path, re_path
|
||||
from django.db.models import Q, F
|
||||
|
||||
from django_filters import rest_framework as rest_filters
|
||||
@ -20,16 +20,68 @@ from InvenTree.helpers import str2bool, DownloadFile
|
||||
from InvenTree.api import AttachmentMixin
|
||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
||||
|
||||
from order.admin import POLineItemResource
|
||||
from order.admin import PurchaseOrderLineItemResource
|
||||
import order.models as models
|
||||
import order.serializers as serializers
|
||||
from part.models import Part
|
||||
from users.models import Owner
|
||||
|
||||
|
||||
class POFilter(rest_filters.FilterSet):
|
||||
class GeneralExtraLineList:
|
||||
"""
|
||||
Custom API filters for the POList endpoint
|
||||
General template for ExtraLine API classes
|
||||
"""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
'order',
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = [
|
||||
rest_filters.DjangoFilterBackend,
|
||||
filters.SearchFilter,
|
||||
filters.OrderingFilter
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
'title',
|
||||
'quantity',
|
||||
'note',
|
||||
'reference',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'title',
|
||||
'quantity',
|
||||
'note',
|
||||
'reference'
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'order',
|
||||
]
|
||||
|
||||
|
||||
class PurchaseOrderFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom API filters for the PurchaseOrderList endpoint
|
||||
"""
|
||||
|
||||
assigned_to_me = rest_filters.BooleanFilter(label='assigned_to_me', method='filter_assigned_to_me')
|
||||
@ -58,16 +110,16 @@ class POFilter(rest_filters.FilterSet):
|
||||
]
|
||||
|
||||
|
||||
class POList(generics.ListCreateAPIView):
|
||||
class PurchaseOrderList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PurchaseOrder objects
|
||||
|
||||
- GET: Return list of PO objects (with filters)
|
||||
- GET: Return list of PurchaseOrder objects (with filters)
|
||||
- POST: Create a new PurchaseOrder object
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
serializer_class = serializers.POSerializer
|
||||
filterset_class = POFilter
|
||||
serializer_class = serializers.PurchaseOrderSerializer
|
||||
filterset_class = PurchaseOrderFilter
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
@ -104,7 +156,7 @@ class POList(generics.ListCreateAPIView):
|
||||
'lines',
|
||||
)
|
||||
|
||||
queryset = serializers.POSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
@ -202,11 +254,11 @@ class POList(generics.ListCreateAPIView):
|
||||
ordering = '-creation_date'
|
||||
|
||||
|
||||
class PODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a PurchaseOrder object """
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
serializer_class = serializers.POSerializer
|
||||
serializer_class = serializers.PurchaseOrderSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
@ -229,12 +281,12 @@ class PODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
'lines',
|
||||
)
|
||||
|
||||
queryset = serializers.POSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class POReceive(generics.CreateAPIView):
|
||||
class PurchaseOrderReceive(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to receive stock items against a purchase order.
|
||||
|
||||
@ -249,7 +301,7 @@ class POReceive(generics.CreateAPIView):
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.none()
|
||||
|
||||
serializer_class = serializers.POReceiveSerializer
|
||||
serializer_class = serializers.PurchaseOrderReceiveSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@ -266,9 +318,9 @@ class POReceive(generics.CreateAPIView):
|
||||
return context
|
||||
|
||||
|
||||
class POLineItemFilter(rest_filters.FilterSet):
|
||||
class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for the POLineItemList endpoint
|
||||
Custom filters for the PurchaseOrderLineItemList endpoint
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@ -318,22 +370,22 @@ class POLineItemFilter(rest_filters.FilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class POLineItemList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of POLineItem objects
|
||||
class PurchaseOrderLineItemList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PurchaseOrderLineItem objects
|
||||
|
||||
- GET: Return a list of PO Line Item objects
|
||||
- GET: Return a list of PurchaseOrder Line Item objects
|
||||
- POST: Create a new PurchaseOrderLineItem object
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.all()
|
||||
serializer_class = serializers.POLineItemSerializer
|
||||
filterset_class = POLineItemFilter
|
||||
serializer_class = serializers.PurchaseOrderLineItemSerializer
|
||||
filterset_class = PurchaseOrderLineItemFilter
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = serializers.POLineItemSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
@ -382,7 +434,7 @@ class POLineItemList(generics.ListCreateAPIView):
|
||||
export_format = str(export_format).strip().lower()
|
||||
|
||||
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||
dataset = POLineItemResource().export(queryset=queryset)
|
||||
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
|
||||
@ -432,30 +484,46 @@ class POLineItemList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
|
||||
class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail API endpoint for PurchaseOrderLineItem object
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.all()
|
||||
serializer_class = serializers.POLineItemSerializer
|
||||
serializer_class = serializers.PurchaseOrderLineItemSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
queryset = serializers.POLineItemSerializer.annotate_queryset(queryset)
|
||||
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class PurchaseOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of PurchaseOrderExtraLine objects.
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderExtraLine.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderExtraLineSerializer
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a PurchaseOrderExtraLine object """
|
||||
|
||||
queryset = models.PurchaseOrderExtraLine.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderExtraLineSerializer
|
||||
|
||||
|
||||
class SalesOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) a SalesOrderAttachment (file upload)
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
serializer_class = serializers.SOAttachmentSerializer
|
||||
serializer_class = serializers.SalesOrderAttachmentSerializer
|
||||
|
||||
filter_backends = [
|
||||
rest_filters.DjangoFilterBackend,
|
||||
@ -466,20 +534,20 @@ class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class SOAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for SalesOrderAttachment
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
serializer_class = serializers.SOAttachmentSerializer
|
||||
serializer_class = serializers.SalesOrderAttachmentSerializer
|
||||
|
||||
|
||||
class SOList(generics.ListCreateAPIView):
|
||||
class SalesOrderList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of SalesOrder objects.
|
||||
|
||||
- GET: Return list of SO objects (with filters)
|
||||
- GET: Return list of SalesOrder objects (with filters)
|
||||
- POST: Create a new SalesOrder
|
||||
"""
|
||||
|
||||
@ -616,7 +684,7 @@ class SOList(generics.ListCreateAPIView):
|
||||
ordering = '-creation_date'
|
||||
|
||||
|
||||
class SODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a SalesOrder object.
|
||||
"""
|
||||
@ -646,9 +714,9 @@ class SODetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
return queryset
|
||||
|
||||
|
||||
class SOLineItemFilter(rest_filters.FilterSet):
|
||||
class SalesOrderLineItemFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for SOLineItemList endpoint
|
||||
Custom filters for SalesOrderLineItemList endpoint
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@ -679,14 +747,14 @@ class SOLineItemFilter(rest_filters.FilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class SOLineItemList(generics.ListCreateAPIView):
|
||||
class SalesOrderLineItemList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of SalesOrderLineItem objects.
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderLineItem.objects.all()
|
||||
serializer_class = serializers.SOLineItemSerializer
|
||||
filterset_class = SOLineItemFilter
|
||||
serializer_class = serializers.SalesOrderLineItemSerializer
|
||||
filterset_class = SalesOrderLineItemFilter
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
@ -743,11 +811,27 @@ class SOLineItemList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
|
||||
class SOLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of SalesOrderExtraLine objects.
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderExtraLine.objects.all()
|
||||
serializer_class = serializers.SalesOrderExtraLineSerializer
|
||||
|
||||
|
||||
class SalesOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a SalesOrderExtraLine object """
|
||||
|
||||
queryset = models.SalesOrderExtraLine.objects.all()
|
||||
serializer_class = serializers.SalesOrderExtraLineSerializer
|
||||
|
||||
|
||||
class SalesOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a SalesOrderLineItem object """
|
||||
|
||||
queryset = models.SalesOrderLineItem.objects.all()
|
||||
serializer_class = serializers.SOLineItemSerializer
|
||||
serializer_class = serializers.SalesOrderLineItemSerializer
|
||||
|
||||
|
||||
class SalesOrderComplete(generics.CreateAPIView):
|
||||
@ -779,7 +863,7 @@ class SalesOrderAllocateSerials(generics.CreateAPIView):
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrder.objects.none()
|
||||
serializer_class = serializers.SOSerialAllocationSerializer
|
||||
serializer_class = serializers.SalesOrderSerialAllocationSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@ -801,11 +885,11 @@ class SalesOrderAllocate(generics.CreateAPIView):
|
||||
API endpoint to allocate stock items against a SalesOrder
|
||||
|
||||
- The SalesOrder is specified in the URL
|
||||
- See the SOShipmentAllocationSerializer class
|
||||
- See the SalesOrderShipmentAllocationSerializer class
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrder.objects.none()
|
||||
serializer_class = serializers.SOShipmentAllocationSerializer
|
||||
serializer_class = serializers.SalesOrderShipmentAllocationSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@ -822,7 +906,7 @@ class SalesOrderAllocate(generics.CreateAPIView):
|
||||
return ctx
|
||||
|
||||
|
||||
class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detali view of a SalesOrderAllocation object
|
||||
"""
|
||||
@ -831,7 +915,7 @@ class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = serializers.SalesOrderAllocationSerializer
|
||||
|
||||
|
||||
class SOAllocationList(generics.ListAPIView):
|
||||
class SalesOrderAllocationList(generics.ListAPIView):
|
||||
"""
|
||||
API endpoint for listing SalesOrderAllocation objects
|
||||
"""
|
||||
@ -909,9 +993,9 @@ class SOAllocationList(generics.ListAPIView):
|
||||
]
|
||||
|
||||
|
||||
class SOShipmentFilter(rest_filters.FilterSet):
|
||||
class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filterset for the SOShipmentList endpoint
|
||||
Custom filterset for the SalesOrderShipmentList endpoint
|
||||
"""
|
||||
|
||||
shipped = rest_filters.BooleanFilter(label='shipped', method='filter_shipped')
|
||||
@ -934,21 +1018,21 @@ class SOShipmentFilter(rest_filters.FilterSet):
|
||||
]
|
||||
|
||||
|
||||
class SOShipmentList(generics.ListCreateAPIView):
|
||||
class SalesOrderShipmentList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API list endpoint for SalesOrderShipment model
|
||||
"""
|
||||
|
||||
queryset = models.SalesOrderShipment.objects.all()
|
||||
serializer_class = serializers.SalesOrderShipmentSerializer
|
||||
filterset_class = SOShipmentFilter
|
||||
filterset_class = SalesOrderShipmentFilter
|
||||
|
||||
filter_backends = [
|
||||
rest_filters.DjangoFilterBackend,
|
||||
]
|
||||
|
||||
|
||||
class SOShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API detail endpooint for SalesOrderShipment model
|
||||
"""
|
||||
@ -957,7 +1041,7 @@ class SOShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = serializers.SalesOrderShipmentSerializer
|
||||
|
||||
|
||||
class SOShipmentComplete(generics.CreateAPIView):
|
||||
class SalesOrderShipmentComplete(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for completing (shipping) a SalesOrderShipment
|
||||
"""
|
||||
@ -983,13 +1067,13 @@ class SOShipmentComplete(generics.CreateAPIView):
|
||||
return ctx
|
||||
|
||||
|
||||
class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
serializer_class = serializers.POAttachmentSerializer
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
|
||||
filter_backends = [
|
||||
rest_filters.DjangoFilterBackend,
|
||||
@ -1000,78 +1084,90 @@ class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class POAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
class PurchaseOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for a PurchaseOrderAttachment
|
||||
"""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
serializer_class = serializers.POAttachmentSerializer
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
|
||||
|
||||
order_api_urls = [
|
||||
|
||||
# API endpoints for purchase orders
|
||||
url(r'^po/', include([
|
||||
re_path(r'^po/', include([
|
||||
|
||||
# Purchase order attachments
|
||||
url(r'attachment/', include([
|
||||
url(r'^(?P<pk>\d+)/$', POAttachmentDetail.as_view(), name='api-po-attachment-detail'),
|
||||
url(r'^.*$', POAttachmentList.as_view(), name='api-po-attachment-list'),
|
||||
re_path(r'attachment/', include([
|
||||
path('<int:pk>/', PurchaseOrderAttachmentDetail.as_view(), name='api-po-attachment-detail'),
|
||||
re_path(r'^.*$', PurchaseOrderAttachmentList.as_view(), name='api-po-attachment-list'),
|
||||
])),
|
||||
|
||||
# Individual purchase order detail URLs
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^receive/', POReceive.as_view(), name='api-po-receive'),
|
||||
url(r'.*$', PODetail.as_view(), name='api-po-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'),
|
||||
re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
|
||||
])),
|
||||
|
||||
# Purchase order list
|
||||
url(r'^.*$', POList.as_view(), name='api-po-list'),
|
||||
re_path(r'^.*$', PurchaseOrderList.as_view(), name='api-po-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for purchase order line items
|
||||
url(r'^po-line/', include([
|
||||
url(r'^(?P<pk>\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'),
|
||||
url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'),
|
||||
re_path(r'^po-line/', include([
|
||||
path('<int:pk>/', PurchaseOrderLineItemDetail.as_view(), name='api-po-line-detail'),
|
||||
re_path(r'^.*$', PurchaseOrderLineItemList.as_view(), name='api-po-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales orders
|
||||
url(r'^so/', include([
|
||||
url(r'attachment/', include([
|
||||
url(r'^(?P<pk>\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'),
|
||||
url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'),
|
||||
# API endpoints for purchase order extra line
|
||||
re_path(r'^po-extra-line/', include([
|
||||
path('<int:pk>/', PurchaseOrderExtraLineDetail.as_view(), name='api-po-extra-line-detail'),
|
||||
path('', PurchaseOrderExtraLineList.as_view(), name='api-po-extra-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales ordesr
|
||||
re_path(r'^so/', include([
|
||||
re_path(r'attachment/', include([
|
||||
path('<int:pk>/', SalesOrderAttachmentDetail.as_view(), name='api-so-attachment-detail'),
|
||||
re_path(r'^.*$', SalesOrderAttachmentList.as_view(), name='api-so-attachment-list'),
|
||||
])),
|
||||
|
||||
url(r'^shipment/', include([
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^ship/$', SOShipmentComplete.as_view(), name='api-so-shipment-ship'),
|
||||
url(r'^.*$', SOShipmentDetail.as_view(), name='api-so-shipment-detail'),
|
||||
re_path(r'^shipment/', include([
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
path('ship/', SalesOrderShipmentComplete.as_view(), name='api-so-shipment-ship'),
|
||||
re_path(r'^.*$', SalesOrderShipmentDetail.as_view(), name='api-so-shipment-detail'),
|
||||
])),
|
||||
url(r'^.*$', SOShipmentList.as_view(), name='api-so-shipment-list'),
|
||||
re_path(r'^.*$', SalesOrderShipmentList.as_view(), name='api-so-shipment-list'),
|
||||
])),
|
||||
|
||||
# Sales order detail view
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
|
||||
url(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
|
||||
url(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),
|
||||
url(r'^.*$', SODetail.as_view(), name='api-so-detail'),
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'),
|
||||
re_path(r'^allocate/', SalesOrderAllocate.as_view(), name='api-so-allocate'),
|
||||
re_path(r'^allocate-serials/', SalesOrderAllocateSerials.as_view(), name='api-so-allocate-serials'),
|
||||
re_path(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'),
|
||||
])),
|
||||
|
||||
# Sales order list view
|
||||
url(r'^.*$', SOList.as_view(), name='api-so-list'),
|
||||
re_path(r'^.*$', SalesOrderList.as_view(), name='api-so-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales order line items
|
||||
url(r'^so-line/', include([
|
||||
url(r'^(?P<pk>\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'),
|
||||
url(r'^$', SOLineItemList.as_view(), name='api-so-line-list'),
|
||||
re_path(r'^so-line/', include([
|
||||
path('<int:pk>/', SalesOrderLineItemDetail.as_view(), name='api-so-line-detail'),
|
||||
path('', SalesOrderLineItemList.as_view(), name='api-so-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales order extra line
|
||||
re_path(r'^so-extra-line/', include([
|
||||
path('<int:pk>/', SalesOrderExtraLineDetail.as_view(), name='api-so-extra-line-detail'),
|
||||
path('', SalesOrderExtraLineList.as_view(), name='api-so-extra-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales order allocations
|
||||
url(r'^so-allocation/', include([
|
||||
url(r'^(?P<pk>\d+)/$', SOAllocationDetail.as_view(), name='api-so-allocation-detail'),
|
||||
url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'),
|
||||
re_path(r'^so-allocation/', include([
|
||||
path('<int:pk>/', SalesOrderAllocationDetail.as_view(), name='api-so-allocation-detail'),
|
||||
re_path(r'^.*$', SalesOrderAllocationList.as_view(), name='api-so-allocation-list'),
|
||||
])),
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ Django Forms for interacting with Order objects
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import InvenTreeMoneyField
|
||||
|
@ -33,7 +33,7 @@ def calculate_shipped_quantity(apps, schema_editor):
|
||||
part=item.part
|
||||
)
|
||||
|
||||
q = sum([item.quantity for item in items])
|
||||
q = sum(item.quantity for item in items)
|
||||
|
||||
item.shipped = q
|
||||
|
||||
|
@ -0,0 +1,109 @@
|
||||
# Generated by Django 3.2.12 on 2022-03-27 01:11
|
||||
|
||||
import InvenTree.fields
|
||||
import django.core.validators
|
||||
from django.core import serializers
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import djmoney.models.fields
|
||||
import djmoney.models.validators
|
||||
|
||||
|
||||
def _convert_model(apps, line_item_ref, extra_line_ref, price_ref):
|
||||
"""Convert the OrderLineItem instances if applicable to new ExtraLine instances"""
|
||||
OrderLineItem = apps.get_model('order', line_item_ref)
|
||||
OrderExtraLine = apps.get_model('order', extra_line_ref)
|
||||
|
||||
items_to_change = OrderLineItem.objects.filter(part=None)
|
||||
if items_to_change.count() == 0:
|
||||
return
|
||||
|
||||
print(f'\nFound {items_to_change.count()} old {line_item_ref} instance(s)')
|
||||
print(f'Starting to convert - currently at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
|
||||
for lineItem in items_to_change:
|
||||
newitem = OrderExtraLine(
|
||||
order=lineItem.order,
|
||||
notes=lineItem.notes,
|
||||
price=getattr(lineItem, price_ref),
|
||||
quantity=lineItem.quantity,
|
||||
reference=lineItem.reference,
|
||||
)
|
||||
newitem.context = {'migration': serializers.serialize('json', [lineItem, ])}
|
||||
newitem.save()
|
||||
|
||||
lineItem.delete()
|
||||
print(f'Done converting line items - now at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
|
||||
|
||||
|
||||
def _reconvert_model(apps, line_item_ref, extra_line_ref):
|
||||
"""Convert ExtraLine instances back to OrderLineItem instances"""
|
||||
OrderLineItem = apps.get_model('order', line_item_ref)
|
||||
OrderExtraLine = apps.get_model('order', extra_line_ref)
|
||||
|
||||
print(f'\nStarting to convert - currently at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
|
||||
for extra_line in OrderExtraLine.objects.all():
|
||||
# regenreate item
|
||||
if extra_line.context:
|
||||
context_string = getattr(extra_line.context, 'migration')
|
||||
if not context_string:
|
||||
continue
|
||||
[item.save() for item in serializers.deserialize('json', context_string)]
|
||||
extra_line.delete()
|
||||
print(f'Done converting line items - now at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)')
|
||||
|
||||
|
||||
def convert_line_items(apps, schema_editor):
|
||||
"""convert line items"""
|
||||
_convert_model(apps, 'PurchaseOrderLineItem', 'PurchaseOrderExtraLine', 'purchase_price')
|
||||
_convert_model(apps, 'SalesOrderLineItem', 'SalesOrderExtraLine', 'sale_price')
|
||||
|
||||
|
||||
def nunconvert_line_items(apps, schema_editor): # pragma: no cover
|
||||
"""reconvert line items"""
|
||||
_reconvert_model(apps, 'PurchaseOrderLineItem', 'PurchaseOrderExtraLine')
|
||||
_reconvert_model(apps, 'SalesOrderLineItem', 'SalesOrderExtraLine')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0063_alter_purchaseorderlineitem_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SalesOrderExtraLine',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
|
||||
('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100, verbose_name='Reference')),
|
||||
('notes', models.CharField(blank=True, help_text='Line item notes', max_length=500, verbose_name='Notes')),
|
||||
('target_date', models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date')),
|
||||
('context', models.JSONField(blank=True, help_text='Additional context for this line', null=True, verbose_name='Context')),
|
||||
('price_currency', djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3)),
|
||||
('price', InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price', max_digits=19, null=True, validators=[djmoney.models.validators.MinMoneyValidator(0)], verbose_name='Price')),
|
||||
('order', models.ForeignKey(help_text='Sales Order', on_delete=django.db.models.deletion.CASCADE, related_name='extra_lines', to='order.salesorder', verbose_name='Order')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PurchaseOrderExtraLine',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Item quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
|
||||
('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100, verbose_name='Reference')),
|
||||
('notes', models.CharField(blank=True, help_text='Line item notes', max_length=500, verbose_name='Notes')),
|
||||
('target_date', models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date')),
|
||||
('context', models.JSONField(blank=True, help_text='Additional context for this line', null=True, verbose_name='Context')),
|
||||
('price_currency', djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3)),
|
||||
('price', InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price', max_digits=19, null=True, validators=[djmoney.models.validators.MinMoneyValidator(0)], verbose_name='Price')),
|
||||
('order', models.ForeignKey(help_text='Purchase Order', on_delete=django.db.models.deletion.CASCADE, related_name='extra_lines', to='order.purchaseorder', verbose_name='Order')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.RunPython(convert_line_items, reverse_code=nunconvert_line_items),
|
||||
]
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.12 on 2022-03-28 22:02
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0042_supplierpricebreak_updated'),
|
||||
('order', '0064_purchaseorderextraline_salesorderextraline'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchaseorderlineitem',
|
||||
name='part',
|
||||
field=models.ForeignKey(help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_order_line_items', to='company.supplierpart', verbose_name='Part'),
|
||||
),
|
||||
]
|
@ -5,6 +5,7 @@ Order model definitions
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
@ -16,11 +17,15 @@ from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from markdownx.models import MarkdownxField
|
||||
from mptt.models import TreeForeignKey
|
||||
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
from common.settings import currency_code_default
|
||||
|
||||
from users import models as UserModels
|
||||
from part import models as PartModels
|
||||
from stock import models as stock_models
|
||||
@ -44,7 +49,7 @@ def get_next_po_number():
|
||||
|
||||
order = PurchaseOrder.objects.exclude(reference=None).last()
|
||||
|
||||
attempts = set([order.reference])
|
||||
attempts = {order.reference}
|
||||
|
||||
reference = order.reference
|
||||
|
||||
@ -73,7 +78,7 @@ def get_next_so_number():
|
||||
|
||||
order = SalesOrder.objects.exclude(reference=None).last()
|
||||
|
||||
attempts = set([order.reference])
|
||||
attempts = {order.reference}
|
||||
|
||||
reference = order.reference
|
||||
|
||||
@ -146,6 +151,25 @@ class Order(ReferenceIndexingMixin):
|
||||
|
||||
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
|
||||
|
||||
def get_total_price(self):
|
||||
"""
|
||||
Calculates the total price of all order lines
|
||||
"""
|
||||
target_currency = currency_code_default()
|
||||
total = Money(0, target_currency)
|
||||
|
||||
# gather name reference
|
||||
price_ref = 'sale_price' if isinstance(self, SalesOrder) else 'purchase_price'
|
||||
# order items
|
||||
total += sum(a.quantity * convert_money(getattr(a, price_ref), target_currency) for a in self.lines.all() if getattr(a, price_ref))
|
||||
|
||||
# extra lines
|
||||
total += sum(a.quantity * convert_money(a.price, target_currency) for a in self.extra_lines.all() if a.price)
|
||||
|
||||
# set decimal-places
|
||||
total.decimal_places = 4
|
||||
return total
|
||||
|
||||
|
||||
class PurchaseOrder(Order):
|
||||
""" A PurchaseOrder represents goods shipped inwards from an external supplier.
|
||||
@ -286,7 +310,7 @@ class PurchaseOrder(Order):
|
||||
raise ValidationError({'supplier': _("Part supplier must match PO supplier")})
|
||||
|
||||
if group:
|
||||
# Check if there is already a matching line item (for this PO)
|
||||
# Check if there is already a matching line item (for this PurchaseOrder)
|
||||
matches = self.lines.filter(part=supplier_part)
|
||||
|
||||
if matches.count() > 0:
|
||||
@ -401,7 +425,7 @@ class PurchaseOrder(Order):
|
||||
@transaction.atomic
|
||||
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs):
|
||||
"""
|
||||
Receive a line item (or partial line item) against this PO
|
||||
Receive a line item (or partial line item) against this PurchaseOrder
|
||||
"""
|
||||
|
||||
# Extract optional batch code for the new stock item
|
||||
@ -852,12 +876,44 @@ class OrderLineItem(models.Model):
|
||||
)
|
||||
|
||||
|
||||
class OrderExtraLine(OrderLineItem):
|
||||
"""
|
||||
Abstract Model for a single ExtraLine in a Order
|
||||
Attributes:
|
||||
price: The unit sale price for this OrderLineItem
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = [
|
||||
]
|
||||
|
||||
context = models.JSONField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Context'),
|
||||
help_text=_('Additional context for this line'),
|
||||
)
|
||||
|
||||
price = InvenTreeModelMoneyField(
|
||||
max_digits=19,
|
||||
decimal_places=4,
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Price'),
|
||||
help_text=_('Unit price'),
|
||||
)
|
||||
|
||||
def price_converted(self):
|
||||
return convert_money(self.price, currency_code_default())
|
||||
|
||||
def price_converted_currency(self):
|
||||
return currency_code_default()
|
||||
|
||||
|
||||
class PurchaseOrderLineItem(OrderLineItem):
|
||||
""" Model for a purchase order line item.
|
||||
|
||||
Attributes:
|
||||
order: Reference to a PurchaseOrder object
|
||||
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@ -904,11 +960,9 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
else:
|
||||
return self.part.part
|
||||
|
||||
# TODO - Function callback for when the SupplierPart is deleted?
|
||||
|
||||
part = models.ForeignKey(
|
||||
SupplierPart, on_delete=models.SET_NULL,
|
||||
blank=True, null=True,
|
||||
blank=False, null=True,
|
||||
related_name='purchase_order_line_items',
|
||||
verbose_name=_('Part'),
|
||||
help_text=_("Supplier part"),
|
||||
@ -961,6 +1015,21 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
return max(r, 0)
|
||||
|
||||
|
||||
class PurchaseOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
Model for a single ExtraLine in a PurchaseOrder
|
||||
Attributes:
|
||||
order: Link to the PurchaseOrder that this line belongs to
|
||||
title: title of line
|
||||
price: The unit price for this OrderLine
|
||||
"""
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-po-extra-line-list')
|
||||
|
||||
order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name='extra_lines', verbose_name=_('Order'), help_text=_('Purchase Order'))
|
||||
|
||||
|
||||
class SalesOrderLineItem(OrderLineItem):
|
||||
"""
|
||||
Model for a single LineItem in a SalesOrder
|
||||
@ -1164,6 +1233,21 @@ class SalesOrderShipment(models.Model):
|
||||
trigger_event('salesordershipment.completed', id=self.pk)
|
||||
|
||||
|
||||
class SalesOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
Model for a single ExtraLine in a SalesOrder
|
||||
Attributes:
|
||||
order: Link to the SalesOrder that this line belongs to
|
||||
title: title of line
|
||||
price: The unit price for this OrderLine
|
||||
"""
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-so-extra-line-list')
|
||||
|
||||
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='extra_lines', verbose_name=_('Order'), help_text=_('Sales Order'))
|
||||
|
||||
|
||||
class SalesOrderAllocation(models.Model):
|
||||
"""
|
||||
This model is used to 'allocate' stock items to a SalesOrder.
|
||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import models, transaction
|
||||
@ -40,7 +40,64 @@ import stock.serializers
|
||||
from users.serializers import OwnerSerializer
|
||||
|
||||
|
||||
class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
class AbstractOrderSerializer(serializers.Serializer):
|
||||
"""
|
||||
Abstract field definitions for OrderSerializers
|
||||
"""
|
||||
total_price = InvenTreeMoneySerializer(
|
||||
source='get_total_price',
|
||||
allow_null=True,
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
total_price_string = serializers.CharField(source='get_total_price', read_only=True)
|
||||
|
||||
|
||||
class AbstractExtraLineSerializer(serializers.Serializer):
|
||||
""" Abstract Serializer for a ExtraLine object """
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if order_detail is not True:
|
||||
self.fields.pop('order_detail')
|
||||
|
||||
quantity = serializers.FloatField()
|
||||
|
||||
price = InvenTreeMoneySerializer(
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
price_string = serializers.CharField(source='price', read_only=True)
|
||||
|
||||
price_currency = serializers.ChoiceField(
|
||||
choices=currency_code_mappings(),
|
||||
help_text=_('Price currency'),
|
||||
)
|
||||
|
||||
|
||||
class AbstractExtraLineMeta:
|
||||
"""
|
||||
Abstract Meta for ExtraLine
|
||||
"""
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'quantity',
|
||||
'reference',
|
||||
'notes',
|
||||
'context',
|
||||
'order',
|
||||
'order_detail',
|
||||
'price',
|
||||
'price_currency',
|
||||
'price_string',
|
||||
]
|
||||
|
||||
|
||||
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
""" Serializer for a PurchaseOrder object """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -110,6 +167,8 @@ class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
'status_text',
|
||||
'target_date',
|
||||
'notes',
|
||||
'total_price',
|
||||
'total_price_string',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -120,7 +179,7 @@ class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
@ -187,7 +246,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
help_text=_('Purchase price currency'),
|
||||
)
|
||||
|
||||
order_detail = POSerializer(source='order', read_only=True, many=False)
|
||||
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
|
||||
|
||||
class Meta:
|
||||
model = order.models.PurchaseOrderLineItem
|
||||
@ -214,7 +273,16 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class POLineItemReceiveSerializer(serializers.Serializer):
|
||||
class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
||||
""" Serializer for a PurchaseOrderExtraLine object """
|
||||
|
||||
order_detail = PurchaseOrderSerializer(source='order', many=False, read_only=True)
|
||||
|
||||
class Meta(AbstractExtraLineMeta):
|
||||
model = order.models.PurchaseOrderExtraLine
|
||||
|
||||
|
||||
class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer for receiving a single purchase order line item against a purchase order
|
||||
"""
|
||||
@ -344,12 +412,12 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
|
||||
class POReceiveSerializer(serializers.Serializer):
|
||||
class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for receiving items against a purchase order
|
||||
"""
|
||||
|
||||
items = POLineItemReceiveSerializer(many=True)
|
||||
items = PurchaseOrderLineItemReceiveSerializer(many=True)
|
||||
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
queryset=stock.models.StockLocation.objects.all(),
|
||||
@ -444,7 +512,7 @@ class POReceiveSerializer(serializers.Serializer):
|
||||
]
|
||||
|
||||
|
||||
class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializers for the PurchaseOrderAttachment model
|
||||
"""
|
||||
@ -467,7 +535,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrder object
|
||||
"""
|
||||
@ -535,6 +603,8 @@ class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSeria
|
||||
'status_text',
|
||||
'shipment_date',
|
||||
'target_date',
|
||||
'total_price',
|
||||
'total_price_string',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -612,7 +682,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SOLineItemSerializer(InvenTreeModelSerializer):
|
||||
class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for a SalesOrderLineItem object """
|
||||
|
||||
@staticmethod
|
||||
@ -862,7 +932,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
order.complete_order(user)
|
||||
|
||||
|
||||
class SOSerialAllocationSerializer(serializers.Serializer):
|
||||
class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for allocation of serial numbers against a sales order / shipment
|
||||
"""
|
||||
@ -1025,7 +1095,7 @@ class SOSerialAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
|
||||
class SOShipmentAllocationSerializer(serializers.Serializer):
|
||||
class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for allocation of stock items against a sales order / shipment
|
||||
"""
|
||||
@ -1099,7 +1169,16 @@ class SOShipmentAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
|
||||
class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
||||
""" Serializer for a SalesOrderExtraLine object """
|
||||
|
||||
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
||||
|
||||
class Meta(AbstractExtraLineMeta):
|
||||
model = order.models.SalesOrderExtraLine
|
||||
|
||||
|
||||
class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrderAttachment model
|
||||
"""
|
||||
|
@ -171,6 +171,12 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{{ order.responsible }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td><span class='fas fa-dollar-sign'></span></td>
|
||||
<td>{% trans "Total cost" %}</td>
|
||||
<td id="poTotalPrice">{{ order.get_total_price }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -42,6 +42,29 @@
|
||||
<table class='table table-striped table-condensed' id='po-line-table' data-toolbar='#order-toolbar-buttons'>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
<h4>{% trans "Extra Lines" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
<div class='btn-group' role='group'>
|
||||
{% if roles.purchase_order.change and order.status == PurchaseOrderStatus.PENDING %}
|
||||
<button type='button' class='btn btn-success' id='new-po-extra-line'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Extra Line" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='order-extra-toolbar-buttons' class='btn-group' style='float: right;'>
|
||||
<div class='btn-group'>
|
||||
{% include "filter_list.html" with id="purchase-order-extra-lines" %}
|
||||
</div>
|
||||
</div>
|
||||
<table class='table table-striped table-condensed' id='po-extra-lines-table' data-toolbar='#order-extra-toolbar-buttons'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-hidden' id='panel-received-items'>
|
||||
@ -200,6 +223,37 @@ loadPurchaseOrderLineItemTable('#po-line-table', {
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
$("#new-po-extra-line").click(function() {
|
||||
|
||||
var fields = extraLineFields({
|
||||
order: {{ order.pk }},
|
||||
});
|
||||
|
||||
constructForm('{% url "api-po-extra-line-list" %}', {
|
||||
fields: fields,
|
||||
method: 'POST',
|
||||
title: '{% trans "Add Order Line" %}',
|
||||
onSuccess: function() {
|
||||
$("#po-extra-lines-table").bootstrapTable("refresh");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
loadPurchaseOrderExtraLineTable(
|
||||
'#po-extra-lines-table',
|
||||
{
|
||||
order: {{ order.pk }},
|
||||
status: {{ order.status }},
|
||||
}
|
||||
);
|
||||
|
||||
loadOrderTotal(
|
||||
'#poTotalPrice',
|
||||
{
|
||||
url: '{% url "api-po-detail" order.pk %}',
|
||||
}
|
||||
);
|
||||
|
||||
enableSidebar('purchaseorder');
|
||||
|
||||
{% endblock %}
|
@ -183,6 +183,12 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{{ order.responsible }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td><span class='fas fa-dollar-sign'></span></td>
|
||||
<td>{% trans "Total cost" %}</td>
|
||||
<td id="soTotalPrice">{{ order.get_total_price }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -34,6 +34,29 @@
|
||||
<table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
<h4>{% trans "Extra Lines" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
<div class='btn-group' role='group'>
|
||||
{% if roles.sales_order.change and order.is_pending %}
|
||||
<button type='button' class='btn btn-success' id='new-so-extra-line'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Extra Line" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='order-extra-toolbar-buttons' class='btn-group' style='float: right;'>
|
||||
<div class='btn-group'>
|
||||
{% include "filter_list.html" with id="sales-order-extra-lines" %}
|
||||
</div>
|
||||
</div>
|
||||
<table class='table table-striped table-condensed' id='so-extra-lines-table' data-toolbar='#order-extra-toolbar-buttons'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if order.is_pending %}
|
||||
@ -238,6 +261,37 @@
|
||||
}
|
||||
);
|
||||
|
||||
$("#new-so-extra-line").click(function() {
|
||||
|
||||
var fields = extraLineFields({
|
||||
order: {{ order.pk }},
|
||||
});
|
||||
|
||||
constructForm('{% url "api-so-extra-line-list" %}', {
|
||||
fields: fields,
|
||||
method: 'POST',
|
||||
title: '{% trans "Add Extra Line" %}',
|
||||
onSuccess: function() {
|
||||
$("#so-extra-lines-table").bootstrapTable("refresh");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
loadSalesOrderExtraLineTable(
|
||||
'#so-extra-lines-table',
|
||||
{
|
||||
order: {{ order.pk }},
|
||||
status: {{ order.status }},
|
||||
}
|
||||
);
|
||||
|
||||
loadOrderTotal(
|
||||
'#soTotalPrice',
|
||||
{
|
||||
url: '{% url "api-so-detail" order.pk %}',
|
||||
}
|
||||
);
|
||||
|
||||
enableSidebar('salesorder');
|
||||
|
||||
{% endblock %}
|
@ -63,7 +63,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
def test_po_list(self):
|
||||
|
||||
# List *ALL* PO items
|
||||
# List *ALL* PurchaseOrder items
|
||||
self.filter({}, 7)
|
||||
|
||||
# Filter by supplier
|
||||
@ -175,7 +175,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
pk = response.data['pk']
|
||||
|
||||
# Try to create a PO with identical reference (should fail!)
|
||||
# Try to create a PurchaseOrder with identical reference (should fail!)
|
||||
response = self.post(
|
||||
url,
|
||||
{
|
||||
@ -493,7 +493,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
|
||||
self.assertIn('can only be received against', str(response.data))
|
||||
|
||||
# Now, set the PO back to "PLACED" so the items can be received
|
||||
# Now, set the PurchaseOrder back to "PLACED" so the items can be received
|
||||
order.status = PurchaseOrderStatus.PLACED
|
||||
order.save()
|
||||
|
||||
|
@ -122,3 +122,97 @@ class TestShipmentMigration(MigratorTestCase):
|
||||
# Check that the correct number of Shipments have been created
|
||||
self.assertEqual(SalesOrder.objects.count(), 5)
|
||||
self.assertEqual(Shipment.objects.count(), 5)
|
||||
|
||||
|
||||
class TestAdditionalLineMigration(MigratorTestCase):
|
||||
"""
|
||||
Test entire schema migration
|
||||
"""
|
||||
|
||||
migrate_from = ('order', '0063_alter_purchaseorderlineitem_unique_together')
|
||||
migrate_to = ('order', '0064_purchaseorderextraline_salesorderextraline')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create initial data set
|
||||
"""
|
||||
|
||||
# Create a purchase order from a supplier
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
PurchaseOrder = self.old_state.apps.get_model('order', 'purchaseorder')
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
Supplierpart = self.old_state.apps.get_model('company', 'supplierpart')
|
||||
# TODO @matmair fix this test!!!
|
||||
# SalesOrder = self.old_state.apps.get_model('order', 'salesorder')
|
||||
|
||||
supplier = Company.objects.create(
|
||||
name='Supplier A',
|
||||
description='A great supplier!',
|
||||
is_supplier=True,
|
||||
is_customer=True,
|
||||
)
|
||||
|
||||
part = Part.objects.create(
|
||||
name='Bob',
|
||||
description='Can we build it?',
|
||||
assembly=True,
|
||||
salable=True,
|
||||
purchaseable=False,
|
||||
tree_id=0,
|
||||
level=0,
|
||||
lft=0,
|
||||
rght=0,
|
||||
)
|
||||
supplierpart = Supplierpart.objects.create(
|
||||
part=part,
|
||||
supplier=supplier
|
||||
)
|
||||
|
||||
# Create some orders
|
||||
for ii in range(10):
|
||||
|
||||
order = PurchaseOrder.objects.create(
|
||||
supplier=supplier,
|
||||
reference=f"{ii}-abcde",
|
||||
description="Just a test order"
|
||||
)
|
||||
order.lines.create(
|
||||
part=supplierpart,
|
||||
quantity=12,
|
||||
received=1
|
||||
)
|
||||
order.lines.create(
|
||||
quantity=12,
|
||||
received=1
|
||||
)
|
||||
|
||||
# TODO @matmair fix this test!!!
|
||||
# sales_order = SalesOrder.objects.create(
|
||||
# customer=supplier,
|
||||
# reference=f"{ii}-xyz",
|
||||
# description="A test sales order",
|
||||
# )
|
||||
# sales_order.lines.create(
|
||||
# part=part,
|
||||
# quantity=12,
|
||||
# received=1
|
||||
# )
|
||||
|
||||
def test_po_migration(self):
|
||||
"""
|
||||
Test that the the PO lines where converted correctly
|
||||
"""
|
||||
|
||||
PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
|
||||
for ii in range(10):
|
||||
|
||||
po = PurchaseOrder.objects.get(reference=f"{ii}-abcde")
|
||||
self.assertEqual(po.extra_lines.count(), 1)
|
||||
self.assertEqual(po.lines.count(), 1)
|
||||
|
||||
# TODO @matmair fix this test!!!
|
||||
# SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
|
||||
# for ii in range(10):
|
||||
# so = SalesOrder.objects.get(reference=f"{ii}-xyz")
|
||||
# self.assertEqual(so.extra_lines, 1)
|
||||
# self.assertEqual(so.lines.count(), 1)
|
||||
|
@ -5,50 +5,50 @@ URL lookup for the Order app. Provides URL endpoints for:
|
||||
- Detail view of Purchase Orders
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import include, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
purchase_order_detail_urls = [
|
||||
|
||||
url(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'),
|
||||
url(r'^issue/', views.PurchaseOrderIssue.as_view(), name='po-issue'),
|
||||
url(r'^complete/', views.PurchaseOrderComplete.as_view(), name='po-complete'),
|
||||
re_path(r'^cancel/', views.PurchaseOrderCancel.as_view(), name='po-cancel'),
|
||||
re_path(r'^issue/', views.PurchaseOrderIssue.as_view(), name='po-issue'),
|
||||
re_path(r'^complete/', views.PurchaseOrderComplete.as_view(), name='po-complete'),
|
||||
|
||||
url(r'^upload/', views.PurchaseOrderUpload.as_view(), name='po-upload'),
|
||||
url(r'^export/', views.PurchaseOrderExport.as_view(), name='po-export'),
|
||||
re_path(r'^upload/', views.PurchaseOrderUpload.as_view(), name='po-upload'),
|
||||
re_path(r'^export/', views.PurchaseOrderExport.as_view(), name='po-export'),
|
||||
|
||||
url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='po-detail'),
|
||||
re_path(r'^.*$', views.PurchaseOrderDetail.as_view(), name='po-detail'),
|
||||
]
|
||||
|
||||
purchase_order_urls = [
|
||||
|
||||
url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'),
|
||||
url(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'),
|
||||
re_path(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'),
|
||||
re_path(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'),
|
||||
|
||||
# Display detail view for a single purchase order
|
||||
url(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)),
|
||||
re_path(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)),
|
||||
|
||||
# Display complete list of purchase orders
|
||||
url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'),
|
||||
re_path(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'),
|
||||
]
|
||||
|
||||
sales_order_detail_urls = [
|
||||
url(r'^cancel/', views.SalesOrderCancel.as_view(), name='so-cancel'),
|
||||
url(r'^export/', views.SalesOrderExport.as_view(), name='so-export'),
|
||||
re_path(r'^cancel/', views.SalesOrderCancel.as_view(), name='so-cancel'),
|
||||
re_path(r'^export/', views.SalesOrderExport.as_view(), name='so-export'),
|
||||
|
||||
url(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'),
|
||||
re_path(r'^.*$', views.SalesOrderDetail.as_view(), name='so-detail'),
|
||||
]
|
||||
|
||||
sales_order_urls = [
|
||||
# Display detail view for a single SalesOrder
|
||||
url(r'^(?P<pk>\d+)/', include(sales_order_detail_urls)),
|
||||
re_path(r'^(?P<pk>\d+)/', include(sales_order_detail_urls)),
|
||||
|
||||
# Display list of all sales orders
|
||||
url(r'^.*$', views.SalesOrderIndex.as_view(), name='so-index'),
|
||||
re_path(r'^.*$', views.SalesOrderIndex.as_view(), name='so-index'),
|
||||
]
|
||||
|
||||
order_urls = [
|
||||
url(r'^purchase-order/', include(purchase_order_urls)),
|
||||
url(r'^sales-order/', include(sales_order_urls)),
|
||||
re_path(r'^purchase-order/', include(purchase_order_urls)),
|
||||
re_path(r'^sales-order/', include(sales_order_urls)),
|
||||
]
|
||||
|
@ -11,7 +11,7 @@ from django.http.response import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.forms import HiddenInput, IntegerField
|
||||
|
||||
@ -20,7 +20,7 @@ from decimal import Decimal, InvalidOperation
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
from .models import SalesOrder, SalesOrderLineItem
|
||||
from .admin import POLineItemResource, SOLineItemResource
|
||||
from .admin import PurchaseOrderLineItemResource, SalesOrderLineItemResource
|
||||
from build.models import Build
|
||||
from company.models import Company, SupplierPart # ManufacturerPart
|
||||
from stock.models import StockItem
|
||||
@ -410,7 +410,7 @@ class SalesOrderExport(AjaxView):
|
||||
|
||||
filename = f"{str(order)} - {order.customer.name}.{export_format}"
|
||||
|
||||
dataset = SOLineItemResource().export(queryset=order.lines.all())
|
||||
dataset = SalesOrderLineItemResource().export(queryset=order.lines.all())
|
||||
|
||||
filedata = dataset.export(format=export_format)
|
||||
|
||||
@ -441,7 +441,7 @@ class PurchaseOrderExport(AjaxView):
|
||||
fmt=export_format
|
||||
)
|
||||
|
||||
dataset = POLineItemResource().export(queryset=order.lines.all())
|
||||
dataset = PurchaseOrderLineItemResource().export(queryset=order.lines.all())
|
||||
|
||||
filedata = dataset.export(format=export_format)
|
||||
|
||||
@ -491,7 +491,7 @@ class OrderParts(AjaxView):
|
||||
return data
|
||||
|
||||
def get_suppliers(self):
|
||||
""" Calculates a list of suppliers which the user will need to create POs for.
|
||||
""" Calculates a list of suppliers which the user will need to create PurchaseOrders for.
|
||||
This is calculated AFTER the user finishes selecting the parts to order.
|
||||
Crucially, get_parts() must be called before get_suppliers()
|
||||
"""
|
||||
|
Reference in New Issue
Block a user