mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2714 from matmair/matmair/issue2694
[FR] Add delivery cost (excluding unit cost that already exists) in PO
This commit is contained in:
commit
433ecdb2c3
@ -637,7 +637,7 @@ class SupplierPart(models.Model):
|
||||
get_price = common.models.get_price
|
||||
|
||||
def open_orders(self):
|
||||
""" Return a database query for PO line items for this SupplierPart,
|
||||
""" Return a database query for PurchaseOrder line items for this SupplierPart,
|
||||
limited to purchase orders that are open / outstanding.
|
||||
"""
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,13 +1084,13 @@ 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 = [
|
||||
@ -1016,39 +1100,45 @@ order_api_urls = [
|
||||
|
||||
# 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'),
|
||||
url(r'^(?P<pk>\d+)/$', PurchaseOrderAttachmentDetail.as_view(), name='api-po-attachment-detail'),
|
||||
url(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'),
|
||||
url(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'),
|
||||
url(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
|
||||
])),
|
||||
|
||||
# Purchase order list
|
||||
url(r'^.*$', POList.as_view(), name='api-po-list'),
|
||||
url(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'),
|
||||
url(r'^(?P<pk>\d+)/$', PurchaseOrderLineItemDetail.as_view(), name='api-po-line-detail'),
|
||||
url(r'^.*$', PurchaseOrderLineItemList.as_view(), name='api-po-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales orders
|
||||
# API endpoints for purchase order extra line
|
||||
url(r'^po-extra-line/', include([
|
||||
url(r'^(?P<pk>\d+)/$', PurchaseOrderExtraLineDetail.as_view(), name='api-po-extra-line-detail'),
|
||||
url(r'^$', PurchaseOrderExtraLineList.as_view(), name='api-po-extra-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales ordesr
|
||||
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'),
|
||||
url(r'^(?P<pk>\d+)/$', SalesOrderAttachmentDetail.as_view(), name='api-so-attachment-detail'),
|
||||
url(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'),
|
||||
url(r'^ship/$', SalesOrderShipmentComplete.as_view(), name='api-so-shipment-ship'),
|
||||
url(r'^.*$', SalesOrderShipmentDetail.as_view(), name='api-so-shipment-detail'),
|
||||
])),
|
||||
url(r'^.*$', SOShipmentList.as_view(), name='api-so-shipment-list'),
|
||||
url(r'^.*$', SalesOrderShipmentList.as_view(), name='api-so-shipment-list'),
|
||||
])),
|
||||
|
||||
# Sales order detail view
|
||||
@ -1056,22 +1146,28 @@ order_api_urls = [
|
||||
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'),
|
||||
url(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'),
|
||||
])),
|
||||
|
||||
# Sales order list view
|
||||
url(r'^.*$', SOList.as_view(), name='api-so-list'),
|
||||
url(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'),
|
||||
url(r'^(?P<pk>\d+)/$', SalesOrderLineItemDetail.as_view(), name='api-so-line-detail'),
|
||||
url(r'^$', SalesOrderLineItemList.as_view(), name='api-so-line-list'),
|
||||
])),
|
||||
|
||||
# API endpoints for sales order extra line
|
||||
url(r'^so-extra-line/', include([
|
||||
url(r'^(?P<pk>\d+)/$', SalesOrderExtraLineDetail.as_view(), name='api-so-extra-line-detail'),
|
||||
url(r'^$', 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'),
|
||||
url(r'^(?P<pk>\d+)/$', SalesOrderAllocationDetail.as_view(), name='api-so-allocation-detail'),
|
||||
url(r'^.*$', SalesOrderAllocationList.as_view(), name='api-so-allocation-list'),
|
||||
])),
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
@ -21,6 +22,10 @@ from django.utils.translation import ugettext_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
|
||||
@ -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.
|
||||
@ -285,7 +309,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:
|
||||
@ -400,7 +424,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
|
||||
@ -851,12 +875,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:
|
||||
@ -903,11 +959,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"),
|
||||
@ -960,6 +1014,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
|
||||
@ -1163,6 +1232,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.
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
"""
|
||||
|
@ -31,8 +31,8 @@ from .models import SalesOrderReport
|
||||
from .serializers import TestReportSerializer
|
||||
from .serializers import BuildReportSerializer
|
||||
from .serializers import BOMReportSerializer
|
||||
from .serializers import POReportSerializer
|
||||
from .serializers import SOReportSerializer
|
||||
from .serializers import PurchaseOrderReportSerializer
|
||||
from .serializers import SalesOrderReportSerializer
|
||||
|
||||
|
||||
class ReportListView(generics.ListAPIView):
|
||||
@ -561,12 +561,12 @@ class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMi
|
||||
return self.print(request, builds)
|
||||
|
||||
|
||||
class POReportList(ReportListView, OrderReportMixin):
|
||||
class PurchaseOrderReportList(ReportListView, OrderReportMixin):
|
||||
|
||||
OrderModel = order.models.PurchaseOrder
|
||||
|
||||
queryset = PurchaseOrderReport.objects.all()
|
||||
serializer_class = POReportSerializer
|
||||
serializer_class = PurchaseOrderReportSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
@ -618,16 +618,16 @@ class POReportList(ReportListView, OrderReportMixin):
|
||||
return queryset
|
||||
|
||||
|
||||
class POReportDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class PurchaseOrderReportDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for a single PurchaseOrderReport object
|
||||
"""
|
||||
|
||||
queryset = PurchaseOrderReport.objects.all()
|
||||
serializer_class = POReportSerializer
|
||||
serializer_class = PurchaseOrderReportSerializer
|
||||
|
||||
|
||||
class POReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
|
||||
class PurchaseOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
|
||||
"""
|
||||
API endpoint for printing a PurchaseOrderReport object
|
||||
"""
|
||||
@ -635,7 +635,7 @@ class POReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin
|
||||
OrderModel = order.models.PurchaseOrder
|
||||
|
||||
queryset = PurchaseOrderReport.objects.all()
|
||||
serializer_class = POReportSerializer
|
||||
serializer_class = PurchaseOrderReportSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@ -644,12 +644,12 @@ class POReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin
|
||||
return self.print(request, orders)
|
||||
|
||||
|
||||
class SOReportList(ReportListView, OrderReportMixin):
|
||||
class SalesOrderReportList(ReportListView, OrderReportMixin):
|
||||
|
||||
OrderModel = order.models.SalesOrder
|
||||
|
||||
queryset = SalesOrderReport.objects.all()
|
||||
serializer_class = SOReportSerializer
|
||||
serializer_class = SalesOrderReportSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
@ -701,16 +701,16 @@ class SOReportList(ReportListView, OrderReportMixin):
|
||||
return queryset
|
||||
|
||||
|
||||
class SOReportDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
class SalesOrderReportDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for a single SalesOrderReport object
|
||||
"""
|
||||
|
||||
queryset = SalesOrderReport.objects.all()
|
||||
serializer_class = SOReportSerializer
|
||||
serializer_class = SalesOrderReportSerializer
|
||||
|
||||
|
||||
class SOReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
|
||||
class SalesOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin):
|
||||
"""
|
||||
API endpoint for printing a PurchaseOrderReport object
|
||||
"""
|
||||
@ -718,7 +718,7 @@ class SOReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin
|
||||
OrderModel = order.models.SalesOrder
|
||||
|
||||
queryset = SalesOrderReport.objects.all()
|
||||
serializer_class = SOReportSerializer
|
||||
serializer_class = SalesOrderReportSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@ -733,23 +733,23 @@ report_api_urls = [
|
||||
url(r'po/', include([
|
||||
# Detail views
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'print/', POReportPrint.as_view(), name='api-po-report-print'),
|
||||
url(r'^$', POReportDetail.as_view(), name='api-po-report-detail'),
|
||||
url(r'print/', PurchaseOrderReportPrint.as_view(), name='api-po-report-print'),
|
||||
url(r'^$', PurchaseOrderReportDetail.as_view(), name='api-po-report-detail'),
|
||||
])),
|
||||
|
||||
# List view
|
||||
url(r'^$', POReportList.as_view(), name='api-po-report-list'),
|
||||
url(r'^$', PurchaseOrderReportList.as_view(), name='api-po-report-list'),
|
||||
])),
|
||||
|
||||
# Sales order reports
|
||||
url(r'so/', include([
|
||||
# Detail views
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'print/', SOReportPrint.as_view(), name='api-so-report-print'),
|
||||
url(r'^$', SOReportDetail.as_view(), name='api-so-report-detail'),
|
||||
url(r'print/', SalesOrderReportPrint.as_view(), name='api-so-report-print'),
|
||||
url(r'^$', SalesOrderReportDetail.as_view(), name='api-so-report-detail'),
|
||||
])),
|
||||
|
||||
url(r'^$', SOReportList.as_view(), name='api-so-report-list'),
|
||||
url(r'^$', SalesOrderReportList.as_view(), name='api-so-report-list'),
|
||||
])),
|
||||
|
||||
# Build reports
|
||||
|
@ -466,6 +466,7 @@ class PurchaseOrderReport(ReportTemplateBase):
|
||||
return {
|
||||
'description': order.description,
|
||||
'lines': order.lines,
|
||||
'extra_lines': order.extra_lines,
|
||||
'order': order,
|
||||
'reference': order.reference,
|
||||
'supplier': order.supplier,
|
||||
@ -505,6 +506,7 @@ class SalesOrderReport(ReportTemplateBase):
|
||||
'customer': order.customer,
|
||||
'description': order.description,
|
||||
'lines': order.lines,
|
||||
'extra_lines': order.extra_lines,
|
||||
'order': order,
|
||||
'prefix': common.models.InvenTreeSetting.get_setting('SALESORDER_REFERENCE_PREFIX'),
|
||||
'reference': order.reference,
|
||||
|
@ -58,7 +58,7 @@ class BOMReportSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class POReportSerializer(InvenTreeModelSerializer):
|
||||
class PurchaseOrderReportSerializer(InvenTreeModelSerializer):
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
@ -74,7 +74,7 @@ class POReportSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SOReportSerializer(InvenTreeModelSerializer):
|
||||
class SalesOrderReportSerializer(InvenTreeModelSerializer):
|
||||
|
||||
template = InvenTreeAttachmentSerializerField(required=True)
|
||||
|
||||
|
@ -38,7 +38,7 @@ from InvenTree.filters import InvenTreeOrderingFilter
|
||||
|
||||
from order.models import PurchaseOrder
|
||||
from order.models import SalesOrder, SalesOrderAllocation
|
||||
from order.serializers import POSerializer
|
||||
from order.serializers import PurchaseOrderSerializer
|
||||
|
||||
from part.models import BomItem, Part, PartCategory
|
||||
from part.serializers import PartBriefSerializer
|
||||
@ -1315,7 +1315,7 @@ class StockTrackingList(generics.ListAPIView):
|
||||
if 'purchaseorder' in deltas:
|
||||
try:
|
||||
order = PurchaseOrder.objects.get(pk=deltas['purchaseorder'])
|
||||
serializer = POSerializer(order)
|
||||
serializer = PurchaseOrderSerializer(order)
|
||||
deltas['purchaseorder_detail'] = serializer.data
|
||||
except:
|
||||
pass
|
||||
|
@ -26,15 +26,19 @@
|
||||
editPurchaseOrderLineItem,
|
||||
exportOrder,
|
||||
loadPurchaseOrderLineItemTable,
|
||||
loadPurchaseOrderExtraLineTable
|
||||
loadPurchaseOrderTable,
|
||||
loadSalesOrderAllocationTable,
|
||||
loadSalesOrderLineItemTable,
|
||||
loadSalesOrderExtraLineTable
|
||||
loadSalesOrderShipmentTable,
|
||||
loadSalesOrderTable,
|
||||
newPurchaseOrderFromOrderWizard,
|
||||
newSupplierPartFromOrderWizard,
|
||||
removeOrderRowFromOrderWizard,
|
||||
removePurchaseOrderLineItem,
|
||||
loadOrderTotal,
|
||||
extraLineFields,
|
||||
*/
|
||||
|
||||
|
||||
@ -272,7 +276,7 @@ function createPurchaseOrder(options={}) {
|
||||
if (options.onSuccess) {
|
||||
options.onSuccess(data);
|
||||
} else {
|
||||
// Default action is to redirect browser to the new PO
|
||||
// Default action is to redirect browser to the new PurchaseOrder
|
||||
location.href = `/order/purchase-order/${data.pk}/`;
|
||||
}
|
||||
},
|
||||
@ -305,6 +309,28 @@ function soLineItemFields(options={}) {
|
||||
}
|
||||
|
||||
|
||||
/* Construct a set of fields for a OrderExtraLine form */
|
||||
function extraLineFields(options={}) {
|
||||
|
||||
var fields = {
|
||||
order: {
|
||||
hidden: true,
|
||||
},
|
||||
quantity: {},
|
||||
reference: {},
|
||||
price: {},
|
||||
price_currency: {},
|
||||
notes: {},
|
||||
};
|
||||
|
||||
if (options.order) {
|
||||
fields.order.value = options.order;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
||||
/* Construct a set of fields for the PurchaseOrderLineItem form */
|
||||
function poLineItemFields(options={}) {
|
||||
|
||||
@ -502,7 +528,7 @@ function newPurchaseOrderFromOrderWizard(e) {
|
||||
|
||||
/**
|
||||
* Receive stock items against a PurchaseOrder
|
||||
* Uses the POReceive API endpoint
|
||||
* Uses the PurchaseOrderReceive API endpoint
|
||||
*
|
||||
* arguments:
|
||||
* - order_id, ID / PK for the PurchaseOrder instance
|
||||
@ -1373,6 +1399,226 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a table displaying lines for a particular PurchaseOrder
|
||||
*
|
||||
* @param {String} table : HTML ID tag e.g. '#table'
|
||||
* @param {Object} options : object which contains:
|
||||
* - order {integer} : pk of the PurchaseOrder
|
||||
* - status: {integer} : status code for the order
|
||||
*/
|
||||
function loadPurchaseOrderExtraLineTable(table, options={}) {
|
||||
|
||||
options.table = table;
|
||||
|
||||
options.params = options.params || {};
|
||||
|
||||
if (!options.order) {
|
||||
console.log('ERROR: function called without order ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.log('ERROR: function called without order status');
|
||||
return;
|
||||
}
|
||||
|
||||
options.params.order = options.order;
|
||||
options.params.part_detail = true;
|
||||
options.params.allocations = true;
|
||||
|
||||
var filters = loadTableFilters('purchaseorderextraline');
|
||||
|
||||
for (var key in options.params) {
|
||||
filters[key] = options.params[key];
|
||||
}
|
||||
|
||||
options.url = options.url || '{% url "api-po-extra-line-list" %}';
|
||||
|
||||
var filter_target = options.filter_target || '#filter-list-purchase-order-extra-lines';
|
||||
|
||||
setupFilterList('purchaseorderextraline', $(table), filter_target);
|
||||
|
||||
// Is the order pending?
|
||||
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||
|
||||
// Table columns to display
|
||||
var columns = [
|
||||
{
|
||||
sortable: true,
|
||||
field: 'reference',
|
||||
title: '{% trans "Reference" %}',
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
footerFormatter: function(data) {
|
||||
return data.map(function(row) {
|
||||
return +row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
},
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'price',
|
||||
title: '{% trans "Unit Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'total_price',
|
||||
sortable: true,
|
||||
title: '{% trans "Total Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price * row.quantity);
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
var total = data.map(function(row) {
|
||||
return +row['price'] * row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
|
||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
|
||||
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(total);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
columns.push({
|
||||
field: 'notes',
|
||||
title: '{% trans "Notes" %}',
|
||||
});
|
||||
|
||||
if (pending) {
|
||||
columns.push({
|
||||
field: 'buttons',
|
||||
switchable: false,
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
reloadTotal();
|
||||
}
|
||||
|
||||
// Configure callback functions once the table is loaded
|
||||
function setupCallbacks() {
|
||||
|
||||
// Callback for duplicating lines
|
||||
$(table).find('.button-duplicate').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
inventreeGet(`/api/order/po-extra-line/${pk}/`, {}, {
|
||||
success: function(data) {
|
||||
|
||||
var fields = extraLineFields();
|
||||
|
||||
constructForm('{% url "api-po-extra-line-list" %}', {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
data: data,
|
||||
title: '{% trans "Duplicate Line" %}',
|
||||
onSuccess: function(response) {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for editing lines
|
||||
$(table).find('.button-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/po-extra-line/${pk}/`, {
|
||||
fields: {
|
||||
quantity: {},
|
||||
reference: {},
|
||||
price: {},
|
||||
price_currency: {},
|
||||
notes: {},
|
||||
},
|
||||
title: '{% trans "Edit Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for deleting lines
|
||||
$(table).find('.button-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/po-extra-line/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(table).inventreeTable({
|
||||
onPostBody: setupCallbacks,
|
||||
name: 'purchaseorderextraline',
|
||||
sidePagination: 'client',
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No matching line" %}';
|
||||
},
|
||||
queryParams: filters,
|
||||
original: options.params,
|
||||
url: options.url,
|
||||
showFooter: true,
|
||||
uniqueId: 'pk',
|
||||
detailViewByClick: false,
|
||||
columns: columns,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load table displaying list of sales orders
|
||||
*/
|
||||
@ -2259,6 +2505,26 @@ function showFulfilledSubTable(index, row, element, options) {
|
||||
});
|
||||
}
|
||||
|
||||
var TotalPriceRef = ''; // reference to total price field
|
||||
var TotalPriceOptions = {}; // options to reload the price
|
||||
|
||||
function loadOrderTotal(reference, options={}) {
|
||||
TotalPriceRef = reference;
|
||||
TotalPriceOptions = options;
|
||||
}
|
||||
|
||||
function reloadTotal() {
|
||||
inventreeGet(
|
||||
TotalPriceOptions.url,
|
||||
{},
|
||||
{
|
||||
success: function(data) {
|
||||
$(TotalPriceRef).html(data.total_price_string);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Load a table displaying line items for a particular SalesOrder
|
||||
@ -2556,6 +2822,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
||||
|
||||
function reloadTable() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
reloadTotal();
|
||||
}
|
||||
|
||||
// Configure callback functions once the table is loaded
|
||||
@ -2765,3 +3032,223 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
||||
columns: columns,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a table displaying lines for a particular SalesOrder
|
||||
*
|
||||
* @param {String} table : HTML ID tag e.g. '#table'
|
||||
* @param {Object} options : object which contains:
|
||||
* - order {integer} : pk of the SalesOrder
|
||||
* - status: {integer} : status code for the order
|
||||
*/
|
||||
function loadSalesOrderExtraLineTable(table, options={}) {
|
||||
|
||||
options.table = table;
|
||||
|
||||
options.params = options.params || {};
|
||||
|
||||
if (!options.order) {
|
||||
console.log('ERROR: function called without order ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.log('ERROR: function called without order status');
|
||||
return;
|
||||
}
|
||||
|
||||
options.params.order = options.order;
|
||||
options.params.part_detail = true;
|
||||
options.params.allocations = true;
|
||||
|
||||
var filters = loadTableFilters('salesorderextraline');
|
||||
|
||||
for (var key in options.params) {
|
||||
filters[key] = options.params[key];
|
||||
}
|
||||
|
||||
options.url = options.url || '{% url "api-so-extra-line-list" %}';
|
||||
|
||||
var filter_target = options.filter_target || '#filter-list-sales-order-extra-lines';
|
||||
|
||||
setupFilterList('salesorderextraline', $(table), filter_target);
|
||||
|
||||
// Is the order pending?
|
||||
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||
|
||||
// Table columns to display
|
||||
var columns = [
|
||||
{
|
||||
sortable: true,
|
||||
field: 'reference',
|
||||
title: '{% trans "Reference" %}',
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
footerFormatter: function(data) {
|
||||
return data.map(function(row) {
|
||||
return +row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
},
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'price',
|
||||
title: '{% trans "Unit Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'total_price',
|
||||
sortable: true,
|
||||
title: '{% trans "Total Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price * row.quantity);
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
var total = data.map(function(row) {
|
||||
return +row['price'] * row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
|
||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
|
||||
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(total);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
columns.push({
|
||||
field: 'notes',
|
||||
title: '{% trans "Notes" %}',
|
||||
});
|
||||
|
||||
if (pending) {
|
||||
columns.push({
|
||||
field: 'buttons',
|
||||
switchable: false,
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
reloadTotal();
|
||||
}
|
||||
|
||||
// Configure callback functions once the table is loaded
|
||||
function setupCallbacks() {
|
||||
|
||||
// Callback for duplicating lines
|
||||
$(table).find('.button-duplicate').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
inventreeGet(`/api/order/so-extra-line/${pk}/`, {}, {
|
||||
success: function(data) {
|
||||
|
||||
var fields = extraLineFields();
|
||||
|
||||
constructForm('{% url "api-so-extra-line-list" %}', {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
data: data,
|
||||
title: '{% trans "Duplicate Line" %}',
|
||||
onSuccess: function(response) {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for editing lines
|
||||
$(table).find('.button-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/so-extra-line/${pk}/`, {
|
||||
fields: {
|
||||
quantity: {},
|
||||
reference: {},
|
||||
price: {},
|
||||
price_currency: {},
|
||||
notes: {},
|
||||
},
|
||||
title: '{% trans "Edit Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for deleting lines
|
||||
$(table).find('.button-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/so-extra-line/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(table).inventreeTable({
|
||||
onPostBody: setupCallbacks,
|
||||
name: 'salesorderextraline',
|
||||
sidePagination: 'client',
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No matching lines" %}';
|
||||
},
|
||||
queryParams: filters,
|
||||
original: options.params,
|
||||
url: options.url,
|
||||
showFooter: true,
|
||||
uniqueId: 'pk',
|
||||
detailViewByClick: false,
|
||||
columns: columns,
|
||||
});
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ function printBomReports(parts) {
|
||||
|
||||
function printPurchaseOrderReports(orders) {
|
||||
/**
|
||||
* Print PO reports for the provided purchase order(s)
|
||||
* Print PurchaseOrder reports for the provided purchase order(s)
|
||||
*/
|
||||
|
||||
if (orders.length == 0) {
|
||||
@ -325,7 +325,7 @@ function printPurchaseOrderReports(orders) {
|
||||
|
||||
function printSalesOrderReports(orders) {
|
||||
/**
|
||||
* Print SO reports for the provided purchase order(s)
|
||||
* Print SalesOrder reports for the provided purchase order(s)
|
||||
*/
|
||||
|
||||
if (orders.length == 0) {
|
||||
|
@ -132,6 +132,7 @@ class RuleSet(models.Model):
|
||||
'order_purchaseorder',
|
||||
'order_purchaseorderattachment',
|
||||
'order_purchaseorderlineitem',
|
||||
'order_purchaseorderextraline',
|
||||
'company_supplierpart',
|
||||
'company_manufacturerpart',
|
||||
'company_manufacturerpartparameter',
|
||||
@ -142,6 +143,7 @@ class RuleSet(models.Model):
|
||||
'order_salesorderallocation',
|
||||
'order_salesorderattachment',
|
||||
'order_salesorderlineitem',
|
||||
'order_salesorderextraline',
|
||||
'order_salesordershipment',
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user