2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 12:35:46 +00:00

Merge remote-tracking branch 'inventree/master' into order-parts-wizard

# Conflicts:
#	InvenTree/order/serializers.py
#	InvenTree/templates/js/translated/model_renderers.js
This commit is contained in:
Oliver Walters
2022-05-02 16:11:11 +10:00
170 changed files with 58651 additions and 45643 deletions

View File

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

View File

@ -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'),
])),
]

View File

@ -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

View File

@ -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

View File

@ -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),
]

View File

@ -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'),
),
]

View File

@ -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.
@ -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.

View File

@ -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):
@ -202,7 +261,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)
def validate(self, data):
@ -245,7 +304,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
"""
@ -375,12 +443,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, required=True)
items = PurchaseOrderLineItemReceiveSerializer(many=True)
location = serializers.PrimaryKeyRelatedField(
queryset=stock.models.StockLocation.objects.all(),
@ -475,7 +543,7 @@ class POReceiveSerializer(serializers.Serializer):
]
class POAttachmentSerializer(InvenTreeAttachmentSerializer):
class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializers for the PurchaseOrderAttachment model
"""
@ -498,7 +566,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
]
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
"""
Serializers for the SalesOrder object
"""
@ -566,6 +634,8 @@ class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSeria
'status_text',
'shipment_date',
'target_date',
'total_price',
'total_price_string',
]
read_only_fields = [
@ -643,7 +713,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
]
class SOLineItemSerializer(InvenTreeModelSerializer):
class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
""" Serializer for a SalesOrderLineItem object """
@staticmethod
@ -893,7 +963,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
"""
@ -1056,7 +1126,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
"""
@ -1130,7 +1200,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
"""

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

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

View File

@ -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)),
]

View File

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