mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Docstring checks in QC checks (#3089)
* Add pre-commit to the stack * exclude static * Add locales to excludes * fix style errors * rename pipeline steps * also wait on precommit * make template matching simpler * Use the same code for python setup everywhere * use step and cache for python setup * move regular settings up into general envs * just use full update * Use invoke instead of static references * make setup actions more similar * use python3 * refactor names to be similar * fix runner version * fix references * remove incidential change * use matrix for os * Github can't do this right now * ignore docstyle errors * Add seperate docstring test * update flake call * do not fail on docstring * refactor setup into workflow * update reference * switch to action * resturcture * add bash statements * remove os from cache * update input checks * make code cleaner * fix boolean * no relative paths * install wheel by python * switch to install * revert back to simple wheel * refactor import export tests * move setup keys back to not disturbe tests * remove docstyle till that is fixed * update references * continue on error * add docstring test * use relativ action references * Change step / job docstrings * update to merge * reformat comments 1 * fix docstrings 2 * fix docstrings 3 * fix docstrings 4 * fix docstrings 5 * fix docstrings 6 * fix docstrings 7 * fix docstrings 8 * fix docstirns 9 * fix docstrings 10 * docstring adjustments * update the remaining docstrings * small docstring changes * fix function name * update support files for docstrings * Add missing args to docstrings * Remove outdated function * Add docstrings for the 'build' app * Make API code cleaner * add more docstrings for plugin app * Remove dead code for plugin settings No idea what that was even intended for * ignore __init__ files for docstrings * More docstrings * Update docstrings for the 'part' directory * Fixes for related_part functionality * Fix removed stuff from merge99676ee
* make more consistent * Show statistics for docstrings * add more docstrings * move specific register statements to make them clearer to understant * More docstrings for common * and more docstrings * and more * simpler call * docstrings for notifications * docstrings for common/tests * Add docs for common/models * Revert "move specific register statements to make them clearer to understant" This reverts commitca96654622
. * use typing here * Revert "Make API code cleaner" This reverts commit24fb68bd3e
. * docstring updates for the 'users' app * Add generic Meta info to simple Meta classes * remove unneeded unique_together statements * More simple metas * Remove unnecessary format specifier * Remove extra json format specifiers * Add docstrings for the 'plugin' app * Docstrings for the 'label' app * Add missing docstrings for the 'report' app * Fix build test regression * Fix top-level files * docstrings for InvenTree/InvenTree * reduce unneeded code * add docstrings * and more docstrings * more docstrings * more docstrings for stock * more docstrings * docstrings for order/views * Docstrings for various files in the 'order' app * Docstrings for order/test_api.py * Docstrings for order/serializers.py * Docstrings for order/admin.py * More docstrings for the order app * Add docstrings for the 'company' app * Add unit tests for rebuilding the reference fields * Prune out some more dead code * remove more dead code Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
@ -1,3 +1 @@
|
||||
"""
|
||||
The Order module is responsible for managing Orders
|
||||
"""
|
||||
"""The Order module is responsible for managing Orders."""
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Admin functionality for the 'order' app"""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
import import_export.widgets as widgets
|
||||
@ -13,6 +15,7 @@ from .models import (PurchaseOrder, PurchaseOrderExtraLine,
|
||||
|
||||
# region general classes
|
||||
class GeneralExtraLineAdmin:
|
||||
"""Admin class template for the 'ExtraLineItem' models"""
|
||||
list_display = (
|
||||
'order',
|
||||
'quantity',
|
||||
@ -29,6 +32,7 @@ class GeneralExtraLineAdmin:
|
||||
|
||||
|
||||
class GeneralExtraLineMeta:
|
||||
"""Metaclass template for the 'ExtraLineItem' models"""
|
||||
skip_unchanged = True
|
||||
report_skipped = False
|
||||
clean_model_instances = True
|
||||
@ -36,11 +40,13 @@ class GeneralExtraLineMeta:
|
||||
|
||||
|
||||
class PurchaseOrderLineItemInlineAdmin(admin.StackedInline):
|
||||
"""Inline admin class for the PurchaseOrderLineItem model"""
|
||||
model = PurchaseOrderLineItem
|
||||
extra = 0
|
||||
|
||||
|
||||
class PurchaseOrderAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the PurchaseOrder model"""
|
||||
|
||||
exclude = [
|
||||
'reference_int',
|
||||
@ -68,6 +74,7 @@ class PurchaseOrderAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class SalesOrderAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrder model"""
|
||||
|
||||
exclude = [
|
||||
'reference_int',
|
||||
@ -91,9 +98,7 @@ class SalesOrderAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class PurchaseOrderResource(ModelResource):
|
||||
"""
|
||||
Class for managing import / export of PurchaseOrder data
|
||||
"""
|
||||
"""Class for managing import / export of PurchaseOrder data."""
|
||||
|
||||
# Add number of line items
|
||||
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
|
||||
@ -102,6 +107,7 @@ class PurchaseOrderResource(ModelResource):
|
||||
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass"""
|
||||
model = PurchaseOrder
|
||||
skip_unchanged = True
|
||||
clean_model_instances = True
|
||||
@ -111,7 +117,7 @@ class PurchaseOrderResource(ModelResource):
|
||||
|
||||
|
||||
class PurchaseOrderLineItemResource(ModelResource):
|
||||
""" Class for managing import / export of PurchaseOrderLineItem data """
|
||||
"""Class for managing import / export of PurchaseOrderLineItem data."""
|
||||
|
||||
part_name = Field(attribute='part__part__name', readonly=True)
|
||||
|
||||
@ -122,6 +128,7 @@ class PurchaseOrderLineItemResource(ModelResource):
|
||||
SKU = Field(attribute='part__SKU', readonly=True)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass"""
|
||||
model = PurchaseOrderLineItem
|
||||
skip_unchanged = True
|
||||
report_skipped = False
|
||||
@ -129,16 +136,16 @@ class PurchaseOrderLineItemResource(ModelResource):
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineResource(ModelResource):
|
||||
""" Class for managing import / export of PurchaseOrderExtraLine data """
|
||||
"""Class for managing import / export of PurchaseOrderExtraLine data."""
|
||||
|
||||
class Meta(GeneralExtraLineMeta):
|
||||
"""Metaclass options."""
|
||||
|
||||
model = PurchaseOrderExtraLine
|
||||
|
||||
|
||||
class SalesOrderResource(ModelResource):
|
||||
"""
|
||||
Class for managing import / export of SalesOrder data
|
||||
"""
|
||||
"""Class for managing import / export of SalesOrder data."""
|
||||
|
||||
# Add number of line items
|
||||
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
|
||||
@ -147,6 +154,7 @@ class SalesOrderResource(ModelResource):
|
||||
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
model = SalesOrder
|
||||
skip_unchanged = True
|
||||
clean_model_instances = True
|
||||
@ -156,9 +164,7 @@ class SalesOrderResource(ModelResource):
|
||||
|
||||
|
||||
class SalesOrderLineItemResource(ModelResource):
|
||||
"""
|
||||
Class for managing import / export of SalesOrderLineItem data
|
||||
"""
|
||||
"""Class for managing import / export of SalesOrderLineItem data."""
|
||||
|
||||
part_name = Field(attribute='part__name', readonly=True)
|
||||
|
||||
@ -169,17 +175,17 @@ class SalesOrderLineItemResource(ModelResource):
|
||||
fulfilled = Field(attribute='fulfilled_quantity', readonly=True)
|
||||
|
||||
def dehydrate_sale_price(self, item):
|
||||
"""
|
||||
Return a string value of the 'sale_price' field, rather than the 'Money' object.
|
||||
"""Return a string value of the 'sale_price' field, rather than the 'Money' object.
|
||||
|
||||
Ref: https://github.com/inventree/InvenTree/issues/2207
|
||||
"""
|
||||
|
||||
if item.sale_price:
|
||||
return str(item.sale_price)
|
||||
else:
|
||||
return ''
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
model = SalesOrderLineItem
|
||||
skip_unchanged = True
|
||||
report_skipped = False
|
||||
@ -187,13 +193,16 @@ class SalesOrderLineItemResource(ModelResource):
|
||||
|
||||
|
||||
class SalesOrderExtraLineResource(ModelResource):
|
||||
""" Class for managing import / export of SalesOrderExtraLine data """
|
||||
"""Class for managing import / export of SalesOrderExtraLine data."""
|
||||
|
||||
class Meta(GeneralExtraLineMeta):
|
||||
"""Metaclass options."""
|
||||
|
||||
model = SalesOrderExtraLine
|
||||
|
||||
|
||||
class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the PurchaseOrderLine model"""
|
||||
|
||||
resource_class = PurchaseOrderLineItemResource
|
||||
|
||||
@ -210,11 +219,12 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
|
||||
"""Admin class for the PurchaseOrderExtraLine model"""
|
||||
resource_class = PurchaseOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrderLine model"""
|
||||
|
||||
resource_class = SalesOrderLineItemResource
|
||||
|
||||
@ -236,11 +246,12 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
|
||||
|
||||
"""Admin class for the SalesOrderExtraLine model"""
|
||||
resource_class = SalesOrderExtraLineResource
|
||||
|
||||
|
||||
class SalesOrderShipmentAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrderShipment model"""
|
||||
|
||||
list_display = [
|
||||
'order',
|
||||
@ -258,6 +269,7 @@ class SalesOrderShipmentAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class SalesOrderAllocationAdmin(ImportExportModelAdmin):
|
||||
"""Admin class for the SalesOrderAllocation model"""
|
||||
|
||||
list_display = (
|
||||
'line',
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON API for the Order app
|
||||
"""
|
||||
"""JSON API for the Order app."""
|
||||
|
||||
from django.db.models import F, Q
|
||||
from django.urls import include, path, re_path
|
||||
@ -24,11 +22,10 @@ from users.models import Owner
|
||||
|
||||
|
||||
class GeneralExtraLineList:
|
||||
"""
|
||||
General template for ExtraLine API classes
|
||||
"""
|
||||
"""General template for ExtraLine API classes."""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return the serializer instance for this endpoint"""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
@ -41,7 +38,7 @@ class GeneralExtraLineList:
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
"""Return the annotated queryset for this endpoint"""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
@ -76,17 +73,12 @@ class GeneralExtraLineList:
|
||||
|
||||
|
||||
class PurchaseOrderFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom API filters for the PurchaseOrderList endpoint
|
||||
"""
|
||||
"""Custom API filters for the PurchaseOrderList endpoint."""
|
||||
|
||||
assigned_to_me = rest_filters.BooleanFilter(label='assigned_to_me', method='filter_assigned_to_me')
|
||||
|
||||
def filter_assigned_to_me(self, queryset, name, value):
|
||||
"""
|
||||
Filter by orders which are assigned to the current user
|
||||
"""
|
||||
|
||||
"""Filter by orders which are assigned to the current user."""
|
||||
value = str2bool(value)
|
||||
|
||||
# Work out who "me" is!
|
||||
@ -100,6 +92,8 @@ class PurchaseOrderFilter(rest_filters.FilterSet):
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PurchaseOrder
|
||||
fields = [
|
||||
'supplier',
|
||||
@ -107,7 +101,7 @@ class PurchaseOrderFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PurchaseOrder objects
|
||||
"""API endpoint for accessing a list of PurchaseOrder objects.
|
||||
|
||||
- GET: Return list of PurchaseOrder objects (with filters)
|
||||
- POST: Create a new PurchaseOrder object
|
||||
@ -118,9 +112,7 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
filterset_class = PurchaseOrderFilter
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Save user information on create
|
||||
"""
|
||||
"""Save user information on create."""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
@ -132,7 +124,7 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Return the serializer instance for this endpoint"""
|
||||
try:
|
||||
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', False))
|
||||
except AttributeError:
|
||||
@ -144,7 +136,7 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
"""Return the annotated queryset for this endpoint"""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
@ -157,6 +149,8 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the filtered queryset as a file"""
|
||||
|
||||
dataset = PurchaseOrderResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -166,7 +160,7 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
"""Custom queryset filtering"""
|
||||
# Perform basic filtering
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
@ -260,13 +254,13 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a PurchaseOrder object """
|
||||
"""API endpoint for detail view of a PurchaseOrder object."""
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Return serializer instance for this endpoint"""
|
||||
try:
|
||||
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', False))
|
||||
except AttributeError:
|
||||
@ -278,7 +272,7 @@ class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
@ -292,11 +286,10 @@ class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderContextMixin:
|
||||
""" Mixin to add purchase order object as serializer context variable """
|
||||
"""Mixin to add purchase order object as serializer context variable."""
|
||||
|
||||
def get_serializer_context(self):
|
||||
""" Add the PurchaseOrder object to the serializer context """
|
||||
|
||||
"""Add the PurchaseOrder object to the serializer context."""
|
||||
context = super().get_serializer_context()
|
||||
|
||||
# Pass the purchase order through to the serializer for validation
|
||||
@ -311,8 +304,7 @@ class PurchaseOrderContextMixin:
|
||||
|
||||
|
||||
class PurchaseOrderCancel(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to 'cancel' a purchase order.
|
||||
"""API endpoint to 'cancel' a purchase order.
|
||||
|
||||
The purchase order must be in a state which can be cancelled
|
||||
"""
|
||||
@ -323,9 +315,7 @@ class PurchaseOrderCancel(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderComplete(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to 'complete' a purchase order
|
||||
"""
|
||||
"""API endpoint to 'complete' a purchase order."""
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
|
||||
@ -333,9 +323,7 @@ class PurchaseOrderComplete(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to 'complete' a purchase order
|
||||
"""
|
||||
"""API endpoint to 'complete' a purchase order."""
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
|
||||
@ -343,17 +331,17 @@ class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderMetadata(generics.RetrieveUpdateAPIView):
|
||||
"""API endpoint for viewing / updating PurchaseOrder metadata"""
|
||||
"""API endpoint for viewing / updating PurchaseOrder metadata."""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return MetadataSerializer instance for a PurchaseOrder"""
|
||||
return MetadataSerializer(models.PurchaseOrder, *args, **kwargs)
|
||||
|
||||
queryset = models.PurchaseOrder.objects.all()
|
||||
|
||||
|
||||
class PurchaseOrderReceive(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to receive stock items against a purchase order.
|
||||
"""API endpoint to receive stock items against a purchase order.
|
||||
|
||||
- The purchase order is specified in the URL.
|
||||
- Items to receive are specified as a list called "items" with the following options:
|
||||
@ -370,11 +358,11 @@ class PurchaseOrderReceive(PurchaseOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for the PurchaseOrderLineItemList endpoint
|
||||
"""
|
||||
"""Custom filters for the PurchaseOrderLineItemList endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.PurchaseOrderLineItem
|
||||
fields = [
|
||||
'order',
|
||||
@ -384,10 +372,7 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
|
||||
pending = rest_filters.BooleanFilter(label='pending', method='filter_pending')
|
||||
|
||||
def filter_pending(self, queryset, name, value):
|
||||
"""
|
||||
Filter by "pending" status (order status = pending)
|
||||
"""
|
||||
|
||||
"""Filter by "pending" status (order status = pending)"""
|
||||
value = str2bool(value)
|
||||
|
||||
if value:
|
||||
@ -402,12 +387,10 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
|
||||
received = rest_filters.BooleanFilter(label='received', method='filter_received')
|
||||
|
||||
def filter_received(self, queryset, name, value):
|
||||
"""
|
||||
Filter by lines which are "received" (or "not" received)
|
||||
"""Filter by lines which are "received" (or "not" received)
|
||||
|
||||
A line is considered "received" when received >= quantity
|
||||
"""
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
q = Q(received__gte=F('quantity'))
|
||||
@ -422,7 +405,7 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PurchaseOrderLineItem objects
|
||||
"""API endpoint for accessing a list of PurchaseOrderLineItem objects.
|
||||
|
||||
- GET: Return a list of PurchaseOrder Line Item objects
|
||||
- POST: Create a new PurchaseOrderLineItem object
|
||||
@ -433,7 +416,7 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
filterset_class = PurchaseOrderLineItemFilter
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(queryset)
|
||||
@ -441,7 +424,7 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Return serializer instance for this endpoint"""
|
||||
try:
|
||||
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False))
|
||||
kwargs['order_detail'] = str2bool(self.request.query_params.get('order_detail', False))
|
||||
@ -453,10 +436,7 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Additional filtering options
|
||||
"""
|
||||
|
||||
"""Additional filtering options."""
|
||||
params = self.request.query_params
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
@ -475,6 +455,8 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the requested queryset as a file"""
|
||||
|
||||
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -483,19 +465,6 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
filter_backends = [
|
||||
rest_filters.DjangoFilterBackend,
|
||||
filters.SearchFilter,
|
||||
@ -530,15 +499,13 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail API endpoint for PurchaseOrderLineItem object
|
||||
"""
|
||||
"""Detail API endpoint for PurchaseOrderLineItem object."""
|
||||
|
||||
queryset = models.PurchaseOrderLineItem.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderLineItemSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(queryset)
|
||||
@ -547,25 +514,21 @@ class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of PurchaseOrderExtraLine objects.
|
||||
"""
|
||||
"""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 """
|
||||
"""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)
|
||||
"""
|
||||
"""API endpoint for listing (and creating) a SalesOrderAttachment (file upload)"""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
serializer_class = serializers.SalesOrderAttachmentSerializer
|
||||
@ -580,17 +543,14 @@ class SalesOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
|
||||
|
||||
class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for SalesOrderAttachment
|
||||
"""
|
||||
"""Detail endpoint for SalesOrderAttachment."""
|
||||
|
||||
queryset = models.SalesOrderAttachment.objects.all()
|
||||
serializer_class = serializers.SalesOrderAttachmentSerializer
|
||||
|
||||
|
||||
class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of SalesOrder objects.
|
||||
"""API endpoint for accessing a list of SalesOrder objects.
|
||||
|
||||
- GET: Return list of SalesOrder objects (with filters)
|
||||
- POST: Create a new SalesOrder
|
||||
@ -600,9 +560,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
serializer_class = serializers.SalesOrderSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Save user information on create
|
||||
"""
|
||||
"""Save user information on create."""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
@ -614,7 +572,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Return serializer instance for this endpoint"""
|
||||
try:
|
||||
kwargs['customer_detail'] = str2bool(self.request.query_params.get('customer_detail', False))
|
||||
except AttributeError:
|
||||
@ -626,7 +584,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
@ -639,6 +597,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download this queryset as a file"""
|
||||
dataset = SalesOrderResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -648,10 +607,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Perform custom filtering operations on the SalesOrder queryset.
|
||||
"""
|
||||
|
||||
"""Perform custom filtering operations on the SalesOrder queryset."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -739,15 +695,13 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a SalesOrder object.
|
||||
"""
|
||||
"""API endpoint for detail view of a SalesOrder object."""
|
||||
|
||||
queryset = models.SalesOrder.objects.all()
|
||||
serializer_class = serializers.SalesOrderSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Return the serializer instance for this endpoint"""
|
||||
try:
|
||||
kwargs['customer_detail'] = str2bool(self.request.query_params.get('customer_detail', False))
|
||||
except AttributeError:
|
||||
@ -758,7 +712,7 @@ class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
"""Return the annotated queryset for this serializer"""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related('customer', 'lines')
|
||||
@ -769,11 +723,11 @@ class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
class SalesOrderLineItemFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for SalesOrderLineItemList endpoint
|
||||
"""
|
||||
"""Custom filters for SalesOrderLineItemList endpoint."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.SalesOrderLineItem
|
||||
fields = [
|
||||
'order',
|
||||
@ -783,12 +737,10 @@ class SalesOrderLineItemFilter(rest_filters.FilterSet):
|
||||
completed = rest_filters.BooleanFilter(label='completed', method='filter_completed')
|
||||
|
||||
def filter_completed(self, queryset, name, value):
|
||||
"""
|
||||
Filter by lines which are "completed"
|
||||
"""Filter by lines which are "completed".
|
||||
|
||||
A line is completed when shipped >= quantity
|
||||
"""
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
q = Q(shipped__gte=F('quantity'))
|
||||
@ -802,16 +754,14 @@ class SalesOrderLineItemFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class SalesOrderLineItemList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of SalesOrderLineItem objects.
|
||||
"""
|
||||
"""API endpoint for accessing a list of SalesOrderLineItem objects."""
|
||||
|
||||
queryset = models.SalesOrderLineItem.objects.all()
|
||||
serializer_class = serializers.SalesOrderLineItemSerializer
|
||||
filterset_class = SalesOrderLineItemFilter
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Return serializer for this endpoint with extra data as requested"""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
@ -826,7 +776,7 @@ class SalesOrderLineItemList(generics.ListCreateAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
"""Return annotated queryset for this endpoint"""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
@ -866,33 +816,31 @@ class SalesOrderLineItemList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class SalesOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of SalesOrderExtraLine objects.
|
||||
"""
|
||||
"""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 """
|
||||
"""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 """
|
||||
"""API endpoint for detail view of a SalesOrderLineItem object."""
|
||||
|
||||
queryset = models.SalesOrderLineItem.objects.all()
|
||||
serializer_class = serializers.SalesOrderLineItemSerializer
|
||||
|
||||
|
||||
class SalesOrderContextMixin:
|
||||
""" Mixin to add sales order object as serializer context variable """
|
||||
"""Mixin to add sales order object as serializer context variable."""
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
"""Add the 'order' reference to the serializer context for any classes which inherit this mixin"""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
ctx['request'] = self.request
|
||||
@ -906,42 +854,38 @@ class SalesOrderContextMixin:
|
||||
|
||||
|
||||
class SalesOrderCancel(SalesOrderContextMixin, generics.CreateAPIView):
|
||||
"""API endpoint to cancel a SalesOrder"""
|
||||
|
||||
queryset = models.SalesOrder.objects.all()
|
||||
serializer_class = serializers.SalesOrderCancelSerializer
|
||||
|
||||
|
||||
class SalesOrderComplete(SalesOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for manually marking a SalesOrder as "complete".
|
||||
"""
|
||||
"""API endpoint for manually marking a SalesOrder as "complete"."""
|
||||
|
||||
queryset = models.SalesOrder.objects.all()
|
||||
serializer_class = serializers.SalesOrderCompleteSerializer
|
||||
|
||||
|
||||
class SalesOrderMetadata(generics.RetrieveUpdateAPIView):
|
||||
"""API endpoint for viewing / updating SalesOrder metadata"""
|
||||
"""API endpoint for viewing / updating SalesOrder metadata."""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return a metadata serializer for the SalesOrder model"""
|
||||
return MetadataSerializer(models.SalesOrder, *args, **kwargs)
|
||||
|
||||
queryset = models.SalesOrder.objects.all()
|
||||
|
||||
|
||||
class SalesOrderAllocateSerials(SalesOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to allocation stock items against a SalesOrder,
|
||||
by specifying serial numbers.
|
||||
"""
|
||||
"""API endpoint to allocation stock items against a SalesOrder, by specifying serial numbers."""
|
||||
|
||||
queryset = models.SalesOrder.objects.none()
|
||||
serializer_class = serializers.SalesOrderSerialAllocationSerializer
|
||||
|
||||
|
||||
class SalesOrderAllocate(SalesOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to allocate stock items against a SalesOrder
|
||||
"""API endpoint to allocate stock items against a SalesOrder.
|
||||
|
||||
- The SalesOrder is specified in the URL
|
||||
- See the SalesOrderShipmentAllocationSerializer class
|
||||
@ -952,24 +896,23 @@ class SalesOrderAllocate(SalesOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class SalesOrderAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detali view of a SalesOrderAllocation object
|
||||
"""
|
||||
"""API endpoint for detali view of a SalesOrderAllocation object."""
|
||||
|
||||
queryset = models.SalesOrderAllocation.objects.all()
|
||||
serializer_class = serializers.SalesOrderAllocationSerializer
|
||||
|
||||
|
||||
class SalesOrderAllocationList(generics.ListAPIView):
|
||||
"""
|
||||
API endpoint for listing SalesOrderAllocation objects
|
||||
"""
|
||||
"""API endpoint for listing SalesOrderAllocation objects."""
|
||||
|
||||
queryset = models.SalesOrderAllocation.objects.all()
|
||||
serializer_class = serializers.SalesOrderAllocationSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return the serializer instance for this endpoint.
|
||||
|
||||
Adds extra detail serializers if requested
|
||||
"""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
@ -984,7 +927,7 @@ class SalesOrderAllocationList(generics.ListAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
"""Custom queryset filtering"""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
# Filter by order
|
||||
@ -1039,14 +982,12 @@ class SalesOrderAllocationList(generics.ListAPIView):
|
||||
|
||||
|
||||
class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filterset for the SalesOrderShipmentList endpoint
|
||||
"""
|
||||
"""Custom filterset for the SalesOrderShipmentList endpoint."""
|
||||
|
||||
shipped = rest_filters.BooleanFilter(label='shipped', method='filter_shipped')
|
||||
|
||||
def filter_shipped(self, queryset, name, value):
|
||||
|
||||
"""Filter SalesOrder list by 'shipped' status (boolean)"""
|
||||
value = str2bool(value)
|
||||
|
||||
if value:
|
||||
@ -1057,6 +998,8 @@ class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = models.SalesOrderShipment
|
||||
fields = [
|
||||
'order',
|
||||
@ -1064,9 +1007,7 @@ class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class SalesOrderShipmentList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API list endpoint for SalesOrderShipment model
|
||||
"""
|
||||
"""API list endpoint for SalesOrderShipment model."""
|
||||
|
||||
queryset = models.SalesOrderShipment.objects.all()
|
||||
serializer_class = serializers.SalesOrderShipmentSerializer
|
||||
@ -1078,27 +1019,20 @@ class SalesOrderShipmentList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class SalesOrderShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API detail endpooint for SalesOrderShipment model
|
||||
"""
|
||||
"""API detail endpooint for SalesOrderShipment model."""
|
||||
|
||||
queryset = models.SalesOrderShipment.objects.all()
|
||||
serializer_class = serializers.SalesOrderShipmentSerializer
|
||||
|
||||
|
||||
class SalesOrderShipmentComplete(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for completing (shipping) a SalesOrderShipment
|
||||
"""
|
||||
"""API endpoint for completing (shipping) a SalesOrderShipment."""
|
||||
|
||||
queryset = models.SalesOrderShipment.objects.all()
|
||||
serializer_class = serializers.SalesOrderShipmentCompleteSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Pass the request object to the serializer
|
||||
"""
|
||||
|
||||
"""Pass the request object to the serializer."""
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['request'] = self.request
|
||||
|
||||
@ -1113,9 +1047,7 @@ class SalesOrderShipmentComplete(generics.CreateAPIView):
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)
|
||||
"""
|
||||
"""API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)"""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
@ -1130,9 +1062,7 @@ class PurchaseOrderAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for a PurchaseOrderAttachment
|
||||
"""
|
||||
"""Detail endpoint for a PurchaseOrderAttachment."""
|
||||
|
||||
queryset = models.PurchaseOrderAttachment.objects.all()
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Config for the 'order' app"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrderConfig(AppConfig):
|
||||
"""Configuration class for the 'order' app"""
|
||||
name = 'order'
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django Forms for interacting with Order objects
|
||||
"""
|
||||
"""Django Forms for interacting with Order objects."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -11,11 +9,10 @@ from InvenTree.helpers import clean_decimal
|
||||
|
||||
|
||||
class OrderMatchItemForm(MatchItemForm):
|
||||
""" Override MatchItemForm fields """
|
||||
"""Override MatchItemForm fields."""
|
||||
|
||||
def get_special_field(self, col_guess, row, file_manager):
|
||||
""" Set special fields """
|
||||
|
||||
"""Set special fields."""
|
||||
# set quantity field
|
||||
if 'quantity' in col_guess.lower():
|
||||
return forms.CharField(
|
||||
|
@ -1,8 +1,4 @@
|
||||
"""
|
||||
Order model definitions
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Order model definitions."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
@ -47,10 +43,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def get_next_po_number():
|
||||
"""
|
||||
Returns the next available PurchaseOrder reference number
|
||||
"""
|
||||
|
||||
"""Returns the next available PurchaseOrder reference number."""
|
||||
if PurchaseOrder.objects.count() == 0:
|
||||
return '0001'
|
||||
|
||||
@ -76,10 +69,7 @@ def get_next_po_number():
|
||||
|
||||
|
||||
def get_next_so_number():
|
||||
"""
|
||||
Returns the next available SalesOrder reference number
|
||||
"""
|
||||
|
||||
"""Returns the next available SalesOrder reference number."""
|
||||
if SalesOrder.objects.count() == 0:
|
||||
return '0001'
|
||||
|
||||
@ -105,7 +95,7 @@ def get_next_so_number():
|
||||
|
||||
|
||||
class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
""" Abstract model for an order.
|
||||
"""Abstract model for an order.
|
||||
|
||||
Instances of this class:
|
||||
|
||||
@ -123,7 +113,10 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
"""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom save method for the order models:
|
||||
|
||||
Ensures that the reference field is rebuilt whenever the instance is saved.
|
||||
"""
|
||||
self.rebuild_reference_field()
|
||||
|
||||
if not self.creation_date:
|
||||
@ -132,6 +125,8 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options. Abstract ensures no database table is created."""
|
||||
|
||||
abstract = True
|
||||
|
||||
description = models.CharField(max_length=250, verbose_name=_('Description'), help_text=_('Order description'))
|
||||
@ -159,15 +154,13 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
|
||||
|
||||
def get_total_price(self, target_currency=currency_code_default()):
|
||||
"""
|
||||
Calculates the total price of all order lines, and converts to the specified target currency.
|
||||
"""Calculates the total price of all order lines, and converts to the specified target currency.
|
||||
|
||||
If not specified, the default system currency is used.
|
||||
|
||||
If currency conversion fails (e.g. there are no valid conversion rates),
|
||||
then we simply return zero, rather than attempting some other calculation.
|
||||
"""
|
||||
|
||||
total = Money(0, target_currency)
|
||||
|
||||
# gather name reference
|
||||
@ -230,7 +223,7 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
|
||||
|
||||
class PurchaseOrder(Order):
|
||||
""" A PurchaseOrder represents goods shipped inwards from an external supplier.
|
||||
"""A PurchaseOrder represents goods shipped inwards from an external supplier.
|
||||
|
||||
Attributes:
|
||||
supplier: Reference to the company supplying the goods in the order
|
||||
@ -241,14 +234,14 @@ class PurchaseOrder(Order):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrder model"""
|
||||
return reverse('api-po-list')
|
||||
|
||||
OVERDUE_FILTER = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||
|
||||
@staticmethod
|
||||
def filterByDate(queryset, min_date, max_date):
|
||||
"""
|
||||
Filter by 'minimum and maximum date range'
|
||||
"""Filter by 'minimum and maximum date range'.
|
||||
|
||||
- Specified as min_date, max_date
|
||||
- Both must be specified for filter to be applied
|
||||
@ -259,7 +252,6 @@ class PurchaseOrder(Order):
|
||||
- A "pending" order where the target date lies within the date range
|
||||
- TODO: An "overdue" order where the target date is in the past
|
||||
"""
|
||||
|
||||
date_fmt = '%Y-%m-%d' # ISO format date string
|
||||
|
||||
# Ensure that both dates are valid
|
||||
@ -283,7 +275,7 @@ class PurchaseOrder(Order):
|
||||
return queryset
|
||||
|
||||
def __str__(self):
|
||||
|
||||
"""Render a string representation of this PurchaseOrder"""
|
||||
prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX')
|
||||
|
||||
return f"{prefix}{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
|
||||
@ -340,22 +332,29 @@ class PurchaseOrder(Order):
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the web URL of the detail view for this order"""
|
||||
return reverse('po-detail', kwargs={'pk': self.id})
|
||||
|
||||
@transaction.atomic
|
||||
def add_line_item(self, supplier_part, quantity, group=True, reference='', purchase_price=None):
|
||||
""" Add a new line item to this purchase order.
|
||||
This function will check that:
|
||||
def add_line_item(self, supplier_part, quantity, group: bool = True, reference: str = '', purchase_price=None):
|
||||
"""Add a new line item to this purchase order.
|
||||
|
||||
This function will check that:
|
||||
* The supplier part matches the supplier specified for this purchase order
|
||||
* The quantity is greater than zero
|
||||
|
||||
Args:
|
||||
supplier_part - The supplier_part to add
|
||||
quantity - The number of items to add
|
||||
group - If True, this new quantity will be added to an existing line item for the same supplier_part (if it exists)
|
||||
"""
|
||||
supplier_part: The supplier_part to add
|
||||
quantity : The number of items to add
|
||||
group (bool, optional): If True, this new quantity will be added to an existing line item for the same supplier_part (if it exists). Defaults to True.
|
||||
reference (str, optional): Reference to item. Defaults to ''.
|
||||
purchase_price (optional): Price of item. Defaults to None.
|
||||
|
||||
Raises:
|
||||
ValidationError: quantity is smaller than 0
|
||||
ValidationError: quantity is not type int
|
||||
ValidationError: supplier is not supplier of purchase order
|
||||
"""
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
if quantity <= 0:
|
||||
@ -396,8 +395,10 @@ class PurchaseOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def place_order(self):
|
||||
""" Marks the PurchaseOrder as PLACED. Order must be currently PENDING. """
|
||||
"""Marks the PurchaseOrder as PLACED.
|
||||
|
||||
Order must be currently PENDING.
|
||||
"""
|
||||
if self.status == PurchaseOrderStatus.PENDING:
|
||||
self.status = PurchaseOrderStatus.PLACED
|
||||
self.issue_date = datetime.now().date()
|
||||
@ -407,8 +408,10 @@ class PurchaseOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def complete_order(self):
|
||||
""" Marks the PurchaseOrder as COMPLETE. Order must be currently PLACED. """
|
||||
"""Marks the PurchaseOrder as COMPLETE.
|
||||
|
||||
Order must be currently PLACED.
|
||||
"""
|
||||
if self.status == PurchaseOrderStatus.PLACED:
|
||||
self.status = PurchaseOrderStatus.COMPLETE
|
||||
self.complete_date = datetime.now().date()
|
||||
@ -418,22 +421,21 @@ class PurchaseOrder(Order):
|
||||
|
||||
@property
|
||||
def is_overdue(self):
|
||||
"""
|
||||
Returns True if this PurchaseOrder is "overdue"
|
||||
"""Returns True if this PurchaseOrder is "overdue".
|
||||
|
||||
Makes use of the OVERDUE_FILTER to avoid code duplication.
|
||||
"""
|
||||
|
||||
query = PurchaseOrder.objects.filter(pk=self.pk)
|
||||
query = query.filter(PurchaseOrder.OVERDUE_FILTER)
|
||||
|
||||
return query.exists()
|
||||
|
||||
def can_cancel(self):
|
||||
"""
|
||||
A PurchaseOrder can only be cancelled under the following circumstances:
|
||||
"""
|
||||
"""A PurchaseOrder can only be cancelled under the following circumstances.
|
||||
|
||||
- Status is PLACED
|
||||
- Status is PENDING
|
||||
"""
|
||||
return self.status in [
|
||||
PurchaseOrderStatus.PLACED,
|
||||
PurchaseOrderStatus.PENDING
|
||||
@ -441,8 +443,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_order(self):
|
||||
""" Marks the PurchaseOrder as CANCELLED. """
|
||||
|
||||
"""Marks the PurchaseOrder as CANCELLED."""
|
||||
if self.can_cancel():
|
||||
self.status = PurchaseOrderStatus.CANCELLED
|
||||
self.save()
|
||||
@ -450,43 +451,39 @@ class PurchaseOrder(Order):
|
||||
trigger_event('purchaseorder.cancelled', id=self.pk)
|
||||
|
||||
def pending_line_items(self):
|
||||
""" Return a list of pending line items for this order.
|
||||
"""Return a list of pending line items for this order.
|
||||
|
||||
Any line item where 'received' < 'quantity' will be returned.
|
||||
"""
|
||||
|
||||
return self.lines.filter(quantity__gt=F('received'))
|
||||
|
||||
def completed_line_items(self):
|
||||
"""
|
||||
Return a list of completed line items against this order
|
||||
"""
|
||||
"""Return a list of completed line items against this order."""
|
||||
return self.lines.filter(quantity__lte=F('received'))
|
||||
|
||||
@property
|
||||
def line_count(self):
|
||||
"""Return the total number of line items associated with this order"""
|
||||
return self.lines.count()
|
||||
|
||||
@property
|
||||
def completed_line_count(self):
|
||||
|
||||
"""Return the number of complete line items associated with this order"""
|
||||
return self.completed_line_items().count()
|
||||
|
||||
@property
|
||||
def pending_line_count(self):
|
||||
"""Return the number of pending line items associated with this order"""
|
||||
return self.pending_line_items().count()
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
""" Return True if all line items have been received """
|
||||
|
||||
"""Return True if all line items have been received."""
|
||||
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
||||
|
||||
@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 PurchaseOrder
|
||||
"""
|
||||
|
||||
"""Receive a line item (or partial line item) against this PurchaseOrder."""
|
||||
# Extract optional batch code for the new stock item
|
||||
batch_code = kwargs.get('batch_code', '')
|
||||
|
||||
@ -573,8 +570,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
|
||||
class SalesOrder(Order):
|
||||
"""
|
||||
A SalesOrder represents a list of goods shipped outwards to a customer.
|
||||
"""A SalesOrder represents a list of goods shipped outwards to a customer.
|
||||
|
||||
Attributes:
|
||||
customer: Reference to the company receiving the goods in the order
|
||||
@ -584,14 +580,14 @@ class SalesOrder(Order):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrder model"""
|
||||
return reverse('api-so-list')
|
||||
|
||||
OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||
|
||||
@staticmethod
|
||||
def filterByDate(queryset, min_date, max_date):
|
||||
"""
|
||||
Filter by "minimum and maximum date range"
|
||||
"""Filter by "minimum and maximum date range".
|
||||
|
||||
- Specified as min_date, max_date
|
||||
- Both must be specified for filter to be applied
|
||||
@ -602,7 +598,6 @@ class SalesOrder(Order):
|
||||
- A "pending" order where the target date lies within the date range
|
||||
- TODO: An "overdue" order where the target date is in the past
|
||||
"""
|
||||
|
||||
date_fmt = '%Y-%m-%d' # ISO format date string
|
||||
|
||||
# Ensure that both dates are valid
|
||||
@ -625,19 +620,14 @@ class SalesOrder(Order):
|
||||
|
||||
return queryset
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
self.rebuild_reference_field()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
"""Render a string representation of this SalesOrder"""
|
||||
prefix = getSetting('SALESORDER_REFERENCE_PREFIX')
|
||||
|
||||
return f"{prefix}{self.reference} - {self.customer.name if self.customer else _('deleted')}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the web URL for the detail view of this order"""
|
||||
return reverse('so-detail', kwargs={'pk': self.id})
|
||||
|
||||
reference = models.CharField(
|
||||
@ -682,12 +672,10 @@ class SalesOrder(Order):
|
||||
|
||||
@property
|
||||
def is_overdue(self):
|
||||
"""
|
||||
Returns true if this SalesOrder is "overdue":
|
||||
"""Returns true if this SalesOrder is "overdue".
|
||||
|
||||
Makes use of the OVERDUE_FILTER to avoid code duplication.
|
||||
"""
|
||||
|
||||
query = SalesOrder.objects.filter(pk=self.pk)
|
||||
query = query.filter(SalesOrder.OVERDUE_FILTER)
|
||||
|
||||
@ -695,21 +683,18 @@ class SalesOrder(Order):
|
||||
|
||||
@property
|
||||
def is_pending(self):
|
||||
"""Return True if this order is 'pending'"""
|
||||
return self.status == SalesOrderStatus.PENDING
|
||||
|
||||
@property
|
||||
def stock_allocations(self):
|
||||
"""
|
||||
Return a queryset containing all allocations for this order
|
||||
"""
|
||||
|
||||
"""Return a queryset containing all allocations for this order."""
|
||||
return SalesOrderAllocation.objects.filter(
|
||||
line__in=[line.pk for line in self.lines.all()]
|
||||
)
|
||||
|
||||
def is_fully_allocated(self):
|
||||
""" Return True if all line items are fully allocated """
|
||||
|
||||
"""Return True if all line items are fully allocated."""
|
||||
for line in self.lines.all():
|
||||
if not line.is_fully_allocated():
|
||||
return False
|
||||
@ -717,8 +702,7 @@ class SalesOrder(Order):
|
||||
return True
|
||||
|
||||
def is_over_allocated(self):
|
||||
""" Return true if any lines in the order are over-allocated """
|
||||
|
||||
"""Return true if any lines in the order are over-allocated."""
|
||||
for line in self.lines.all():
|
||||
if line.is_over_allocated():
|
||||
return True
|
||||
@ -726,19 +710,14 @@ class SalesOrder(Order):
|
||||
return False
|
||||
|
||||
def is_completed(self):
|
||||
"""
|
||||
Check if this order is "shipped" (all line items delivered),
|
||||
"""
|
||||
|
||||
"""Check if this order is "shipped" (all line items delivered)."""
|
||||
return self.lines.count() > 0 and all([line.is_completed() for line in self.lines.all()])
|
||||
|
||||
def can_complete(self, raise_error=False):
|
||||
"""
|
||||
Test if this SalesOrder can be completed.
|
||||
"""Test if this SalesOrder can be completed.
|
||||
|
||||
Throws a ValidationError if cannot be completed.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
# Order without line items cannot be completed
|
||||
@ -765,10 +744,7 @@ class SalesOrder(Order):
|
||||
return True
|
||||
|
||||
def complete_order(self, user):
|
||||
"""
|
||||
Mark this order as "complete"
|
||||
"""
|
||||
|
||||
"""Mark this order as "complete."""
|
||||
if not self.can_complete():
|
||||
return False
|
||||
|
||||
@ -783,10 +759,7 @@ class SalesOrder(Order):
|
||||
return True
|
||||
|
||||
def can_cancel(self):
|
||||
"""
|
||||
Return True if this order can be cancelled
|
||||
"""
|
||||
|
||||
"""Return True if this order can be cancelled."""
|
||||
if self.status != SalesOrderStatus.PENDING:
|
||||
return False
|
||||
|
||||
@ -794,13 +767,12 @@ class SalesOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_order(self):
|
||||
"""
|
||||
Cancel this order (only if it is "pending")
|
||||
"""Cancel this order (only if it is "pending").
|
||||
|
||||
Executes:
|
||||
- Mark the order as 'cancelled'
|
||||
- Delete any StockItems which have been allocated
|
||||
"""
|
||||
|
||||
if not self.can_cancel():
|
||||
return False
|
||||
|
||||
@ -817,59 +789,54 @@ class SalesOrder(Order):
|
||||
|
||||
@property
|
||||
def line_count(self):
|
||||
"""Return the total number of lines associated with this order"""
|
||||
return self.lines.count()
|
||||
|
||||
def completed_line_items(self):
|
||||
"""
|
||||
Return a queryset of the completed line items for this order
|
||||
"""
|
||||
"""Return a queryset of the completed line items for this order."""
|
||||
return self.lines.filter(shipped__gte=F('quantity'))
|
||||
|
||||
def pending_line_items(self):
|
||||
"""
|
||||
Return a queryset of the pending line items for this order
|
||||
"""
|
||||
"""Return a queryset of the pending line items for this order."""
|
||||
return self.lines.filter(shipped__lt=F('quantity'))
|
||||
|
||||
@property
|
||||
def completed_line_count(self):
|
||||
"""Return the number of completed lines for this order"""
|
||||
return self.completed_line_items().count()
|
||||
|
||||
@property
|
||||
def pending_line_count(self):
|
||||
"""Return the number of pending (incomplete) lines associated with this order"""
|
||||
return self.pending_line_items().count()
|
||||
|
||||
def completed_shipments(self):
|
||||
"""
|
||||
Return a queryset of the completed shipments for this order
|
||||
"""
|
||||
"""Return a queryset of the completed shipments for this order."""
|
||||
return self.shipments.exclude(shipment_date=None)
|
||||
|
||||
def pending_shipments(self):
|
||||
"""
|
||||
Return a queryset of the pending shipments for this order
|
||||
"""
|
||||
|
||||
"""Return a queryset of the pending shipments for this order."""
|
||||
return self.shipments.filter(shipment_date=None)
|
||||
|
||||
@property
|
||||
def shipment_count(self):
|
||||
"""Return the total number of shipments associated with this order"""
|
||||
return self.shipments.count()
|
||||
|
||||
@property
|
||||
def completed_shipment_count(self):
|
||||
"""Return the number of completed shipments associated with this order"""
|
||||
return self.completed_shipments().count()
|
||||
|
||||
@property
|
||||
def pending_shipment_count(self):
|
||||
"""Return the number of pending shipments associated with this order"""
|
||||
return self.pending_shipments().count()
|
||||
|
||||
|
||||
@receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log')
|
||||
def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs):
|
||||
"""
|
||||
Callback function to be executed after a SalesOrder instance is saved
|
||||
"""
|
||||
"""Callback function to be executed after a SalesOrder instance is saved."""
|
||||
if created and getSetting('SALESORDER_DEFAULT_SHIPMENT'):
|
||||
# A new SalesOrder has just been created
|
||||
|
||||
@ -881,37 +848,37 @@ def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs
|
||||
|
||||
|
||||
class PurchaseOrderAttachment(InvenTreeAttachment):
|
||||
"""
|
||||
Model for storing file attachments against a PurchaseOrder object
|
||||
"""
|
||||
"""Model for storing file attachments against a PurchaseOrder object."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrderAttachment model"""
|
||||
return reverse('api-po-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the directory path where PurchaseOrderAttachment files are located"""
|
||||
return os.path.join("po_files", str(self.order.id))
|
||||
|
||||
order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name="attachments")
|
||||
|
||||
|
||||
class SalesOrderAttachment(InvenTreeAttachment):
|
||||
"""
|
||||
Model for storing file attachments against a SalesOrder object
|
||||
"""
|
||||
"""Model for storing file attachments against a SalesOrder object."""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderAttachment class"""
|
||||
return reverse('api-so-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the directory path where SalesOrderAttachment files are located"""
|
||||
return os.path.join("so_files", str(self.order.id))
|
||||
|
||||
order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='attachments')
|
||||
|
||||
|
||||
class OrderLineItem(models.Model):
|
||||
""" Abstract model for an order line item
|
||||
"""Abstract model for an order line item.
|
||||
|
||||
Attributes:
|
||||
quantity: Number of items
|
||||
@ -929,6 +896,8 @@ class OrderLineItem(models.Model):
|
||||
OVERDUE_FILTER = Q(received__lt=F('quantity')) & ~Q(target_date=None) & Q(target_date__lt=datetime.now().date())
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options. Abstract ensures no database table is created."""
|
||||
|
||||
abstract = True
|
||||
|
||||
quantity = RoundingDecimalField(
|
||||
@ -951,16 +920,16 @@ class OrderLineItem(models.Model):
|
||||
|
||||
|
||||
class OrderExtraLine(OrderLineItem):
|
||||
"""
|
||||
Abstract Model for a single ExtraLine in a Order
|
||||
"""Abstract Model for a single ExtraLine in a Order.
|
||||
|
||||
Attributes:
|
||||
price: The unit sale price for this OrderLineItem
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options. Abstract ensures no database table is created."""
|
||||
|
||||
abstract = True
|
||||
unique_together = [
|
||||
]
|
||||
|
||||
context = models.JSONField(
|
||||
blank=True, null=True,
|
||||
@ -976,30 +945,24 @@ class OrderExtraLine(OrderLineItem):
|
||||
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.
|
||||
"""Model for a purchase order line item.
|
||||
|
||||
Attributes:
|
||||
order: Reference to a PurchaseOrder object
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrderLineItem model"""
|
||||
return reverse('api-po-line-list')
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean method for the PurchaseOrderLineItem model:
|
||||
|
||||
- Ensure the supplier part matches the supplier
|
||||
"""
|
||||
super().clean()
|
||||
|
||||
if self.order.supplier and self.part:
|
||||
@ -1010,6 +973,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
})
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of a PurchaseOrderLineItem instance"""
|
||||
return "{n} x {part} from {supplier} (for {po})".format(
|
||||
n=decimal2string(self.quantity),
|
||||
part=self.part.SKU if self.part else 'unknown part',
|
||||
@ -1024,8 +988,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
)
|
||||
|
||||
def get_base_part(self):
|
||||
"""
|
||||
Return the base part.Part object for the line item
|
||||
"""Return the base part.Part object for the line item.
|
||||
|
||||
Note: Returns None if the SupplierPart is not set!
|
||||
"""
|
||||
@ -1067,14 +1030,12 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
)
|
||||
|
||||
def get_destination(self):
|
||||
"""
|
||||
Show where the line item is or should be placed
|
||||
"""Show where the line item is or should be placed.
|
||||
|
||||
NOTE: If a line item gets split when recieved, only an arbitrary
|
||||
stock items location will be reported as the location for the
|
||||
entire line.
|
||||
"""
|
||||
|
||||
for stock in stock_models.StockItem.objects.filter(supplier_part=self.part, purchase_order=self.order):
|
||||
if stock.location:
|
||||
return stock.location
|
||||
@ -1084,14 +1045,14 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
return self.part.part.default_location
|
||||
|
||||
def remaining(self):
|
||||
""" Calculate the number of items remaining to be received """
|
||||
"""Calculate the number of items remaining to be received."""
|
||||
r = self.quantity - self.received
|
||||
return max(r, 0)
|
||||
|
||||
|
||||
class PurchaseOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
Model for a single ExtraLine in a PurchaseOrder
|
||||
"""Model for a single ExtraLine in a PurchaseOrder.
|
||||
|
||||
Attributes:
|
||||
order: Link to the PurchaseOrder that this line belongs to
|
||||
title: title of line
|
||||
@ -1099,14 +1060,14 @@ class PurchaseOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the PurchaseOrderExtraLine model"""
|
||||
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
|
||||
"""Model for a single LineItem in a SalesOrder.
|
||||
|
||||
Attributes:
|
||||
order: Link to the SalesOrder that this line item belongs to
|
||||
@ -1117,6 +1078,7 @@ class SalesOrderLineItem(OrderLineItem):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderLineItem model"""
|
||||
return reverse('api-so-line-list')
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -1145,52 +1107,39 @@ class SalesOrderLineItem(OrderLineItem):
|
||||
validators=[MinValueValidator(0)]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
]
|
||||
|
||||
def fulfilled_quantity(self):
|
||||
"""
|
||||
Return the total stock quantity fulfilled against this line item.
|
||||
"""
|
||||
|
||||
"""Return the total stock quantity fulfilled against this line item."""
|
||||
query = self.order.stock_items.filter(part=self.part).aggregate(fulfilled=Coalesce(Sum('quantity'), Decimal(0)))
|
||||
|
||||
return query['fulfilled']
|
||||
|
||||
def allocated_quantity(self):
|
||||
""" Return the total stock quantity allocated to this LineItem.
|
||||
"""Return the total stock quantity allocated to this LineItem.
|
||||
|
||||
This is a summation of the quantity of each attached StockItem
|
||||
"""
|
||||
|
||||
query = self.allocations.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0)))
|
||||
|
||||
return query['allocated']
|
||||
|
||||
def is_fully_allocated(self):
|
||||
""" Return True if this line item is fully allocated """
|
||||
|
||||
"""Return True if this line item is fully allocated."""
|
||||
if self.order.status == SalesOrderStatus.SHIPPED:
|
||||
return self.fulfilled_quantity() >= self.quantity
|
||||
|
||||
return self.allocated_quantity() >= self.quantity
|
||||
|
||||
def is_over_allocated(self):
|
||||
""" Return True if this line item is over allocated """
|
||||
"""Return True if this line item is over allocated."""
|
||||
return self.allocated_quantity() > self.quantity
|
||||
|
||||
def is_completed(self):
|
||||
"""
|
||||
Return True if this line item is completed (has been fully shipped)
|
||||
"""
|
||||
|
||||
"""Return True if this line item is completed (has been fully shipped)."""
|
||||
return self.shipped >= self.quantity
|
||||
|
||||
|
||||
class SalesOrderShipment(models.Model):
|
||||
"""
|
||||
The SalesOrderShipment model represents a physical shipment made against a SalesOrder.
|
||||
"""The SalesOrderShipment model represents a physical shipment made against a SalesOrder.
|
||||
|
||||
- Points to a single SalesOrder object
|
||||
- Multiple SalesOrderAllocation objects point to a particular SalesOrderShipment
|
||||
@ -1205,6 +1154,7 @@ class SalesOrderShipment(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model options"""
|
||||
# Shipment reference must be unique for a given sales order
|
||||
unique_together = [
|
||||
'order', 'reference',
|
||||
@ -1212,6 +1162,7 @@ class SalesOrderShipment(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderShipment model"""
|
||||
return reverse('api-so-shipment-list')
|
||||
|
||||
order = models.ForeignKey(
|
||||
@ -1275,10 +1226,11 @@ class SalesOrderShipment(models.Model):
|
||||
)
|
||||
|
||||
def is_complete(self):
|
||||
"""Return True if this shipment has already been completed"""
|
||||
return self.shipment_date is not None
|
||||
|
||||
def check_can_complete(self, raise_error=True):
|
||||
|
||||
"""Check if this shipment is able to be completed"""
|
||||
try:
|
||||
if self.shipment_date:
|
||||
# Shipment has already been sent!
|
||||
@ -1297,14 +1249,13 @@ class SalesOrderShipment(models.Model):
|
||||
|
||||
@transaction.atomic
|
||||
def complete_shipment(self, user, **kwargs):
|
||||
"""
|
||||
Complete this particular shipment:
|
||||
"""Complete this particular shipment.
|
||||
|
||||
Executes:
|
||||
1. Update any stock items associated with this shipment
|
||||
2. Update the "shipped" quantity of all associated line items
|
||||
3. Set the "shipment_date" to now
|
||||
"""
|
||||
|
||||
# Check if the shipment can be completed (throw error if not)
|
||||
self.check_can_complete()
|
||||
|
||||
@ -1343,8 +1294,8 @@ class SalesOrderShipment(models.Model):
|
||||
|
||||
|
||||
class SalesOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
Model for a single ExtraLine in a SalesOrder
|
||||
"""Model for a single ExtraLine in a SalesOrder.
|
||||
|
||||
Attributes:
|
||||
order: Link to the SalesOrder that this line belongs to
|
||||
title: title of line
|
||||
@ -1352,40 +1303,37 @@ class SalesOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderExtraLine model"""
|
||||
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.
|
||||
Items that are "allocated" to a SalesOrder are not yet "attached" to the order,
|
||||
but they will be once the order is fulfilled.
|
||||
"""This model is used to 'allocate' stock items to a SalesOrder. Items that are "allocated" to a SalesOrder are not yet "attached" to the order, but they will be once the order is fulfilled.
|
||||
|
||||
Attributes:
|
||||
line: SalesOrderLineItem reference
|
||||
shipment: SalesOrderShipment reference
|
||||
item: StockItem reference
|
||||
quantity: Quantity to take from the StockItem
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the SalesOrderAllocation model"""
|
||||
return reverse('api-so-allocation-list')
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validate the SalesOrderAllocation object:
|
||||
"""Validate the SalesOrderAllocation object.
|
||||
|
||||
Executes:
|
||||
- Cannot allocate stock to a line item without a part reference
|
||||
- The referenced part must match the part associated with the line item
|
||||
- Allocated quantity cannot exceed the quantity of the stock item
|
||||
- Allocation quantity must be "1" if the StockItem is serialized
|
||||
- Allocation quantity cannot be zero
|
||||
"""
|
||||
|
||||
super().clean()
|
||||
|
||||
errors = {}
|
||||
@ -1452,29 +1400,21 @@ class SalesOrderAllocation(models.Model):
|
||||
|
||||
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Enter stock allocation quantity'))
|
||||
|
||||
def get_serial(self):
|
||||
return self.item.serial
|
||||
|
||||
def get_location(self):
|
||||
"""Return the <pk> value of the location associated with this allocation"""
|
||||
return self.item.location.id if self.item.location else None
|
||||
|
||||
def get_location_path(self):
|
||||
if self.item.location:
|
||||
return self.item.location.pathstring
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_po(self):
|
||||
"""Return the PurchaseOrder associated with this allocation"""
|
||||
return self.item.purchase_order
|
||||
|
||||
def complete_allocation(self, user):
|
||||
"""
|
||||
Complete this allocation (called when the parent SalesOrder is marked as "shipped"):
|
||||
"""Complete this allocation (called when the parent SalesOrder is marked as "shipped").
|
||||
|
||||
Executes:
|
||||
- Determine if the referenced StockItem needs to be "split" (if allocated quantity != stock quantity)
|
||||
- Mark the StockItem as belonging to the Customer (this will remove it from stock)
|
||||
"""
|
||||
|
||||
order = self.line.order
|
||||
|
||||
item = self.item.allocateToCustomer(
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON serializers for the Order API
|
||||
"""
|
||||
"""JSON serializers for the Order API."""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
@ -33,9 +31,8 @@ from users.serializers import OwnerSerializer
|
||||
|
||||
|
||||
class AbstractOrderSerializer(serializers.Serializer):
|
||||
"""
|
||||
Abstract field definitions for OrderSerializers
|
||||
"""
|
||||
"""Abstract field definitions for OrderSerializers."""
|
||||
|
||||
total_price = InvenTreeMoneySerializer(
|
||||
source='get_total_price',
|
||||
allow_null=True,
|
||||
@ -46,9 +43,10 @@ class AbstractOrderSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class AbstractExtraLineSerializer(serializers.Serializer):
|
||||
""" Abstract Serializer for a ExtraLine object """
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Abstract Serializer for a ExtraLine object."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialization routine for the serializer"""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -71,9 +69,7 @@ class AbstractExtraLineSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class AbstractExtraLineMeta:
|
||||
"""
|
||||
Abstract Meta for ExtraLine
|
||||
"""
|
||||
"""Abstract Meta for ExtraLine."""
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
@ -90,10 +86,10 @@ class AbstractExtraLineMeta:
|
||||
|
||||
|
||||
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
""" Serializer for a PurchaseOrder object """
|
||||
"""Serializer for a PurchaseOrder object."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
"""Initialization routine for the serializer"""
|
||||
supplier_detail = kwargs.pop('supplier_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -103,13 +99,11 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add extra information to the queryset
|
||||
"""Add extra information to the queryset.
|
||||
|
||||
- Number of lines in the PurchaseOrder
|
||||
- Overdue status of the PurchaseOrder
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
line_items=SubqueryCount('lines')
|
||||
)
|
||||
@ -138,6 +132,8 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
|
||||
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.PurchaseOrder
|
||||
|
||||
fields = [
|
||||
@ -172,18 +168,15 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
|
||||
|
||||
|
||||
class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for cancelling a PurchaseOrder
|
||||
"""
|
||||
"""Serializer for cancelling a PurchaseOrder."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [],
|
||||
|
||||
def get_context_data(self):
|
||||
"""
|
||||
Return custom context information about the order
|
||||
"""
|
||||
|
||||
"""Return custom context information about the order."""
|
||||
self.order = self.context['order']
|
||||
|
||||
return {
|
||||
@ -191,7 +184,7 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
}
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Save the serializer to 'cancel' the order"""
|
||||
order = self.context['order']
|
||||
|
||||
if not order.can_cancel():
|
||||
@ -201,18 +194,15 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for completing a purchase order
|
||||
"""
|
||||
"""Serializer for completing a purchase order."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def get_context_data(self):
|
||||
"""
|
||||
Custom context information for this serializer
|
||||
"""
|
||||
|
||||
"""Custom context information for this serializer."""
|
||||
order = self.context['order']
|
||||
|
||||
return {
|
||||
@ -220,34 +210,34 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
}
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Save the serializer to 'complete' the order"""
|
||||
order = self.context['order']
|
||||
order.complete_order()
|
||||
|
||||
|
||||
class PurchaseOrderIssueSerializer(serializers.Serializer):
|
||||
""" Serializer for issuing (sending) a purchase order """
|
||||
"""Serializer for issuing (sending) a purchase order."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = []
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Save the serializer to 'place' the order"""
|
||||
order = self.context['order']
|
||||
order.place_order()
|
||||
|
||||
|
||||
class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
"""Serializer class for the PurchaseOrderLineItem model"""
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add some extra annotations to this queryset:
|
||||
"""Add some extra annotations to this queryset:
|
||||
|
||||
- Total price = purchase_price * quantity
|
||||
- "Overdue" status (boolean field)
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
total_price=ExpressionWrapper(
|
||||
F('purchase_price') * F('quantity'),
|
||||
@ -267,7 +257,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
return queryset
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
"""Initialization routine for the serializer"""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
@ -284,14 +274,14 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
quantity = serializers.FloatField(min_value=0, required=True)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
|
||||
"""Validation for the 'quantity' field"""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_("Quantity must be greater than zero"))
|
||||
|
||||
return quantity
|
||||
|
||||
def validate_purchase_order(self, purchase_order):
|
||||
|
||||
"""Validation for the 'purchase_order' field"""
|
||||
if purchase_order.status not in PurchaseOrderStatus.OPEN:
|
||||
raise ValidationError(_('Order is not open'))
|
||||
|
||||
@ -323,7 +313,12 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False)
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
|
||||
- Ensure the supplier_part field is supplied
|
||||
- Ensure the purchase_order field is supplied
|
||||
- Ensure that the supplier_part and supplier references match
|
||||
"""
|
||||
data = super().validate(data)
|
||||
|
||||
supplier_part = data.get('part', None)
|
||||
@ -349,6 +344,8 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.PurchaseOrderLineItem
|
||||
|
||||
fields = [
|
||||
@ -374,20 +371,22 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
||||
""" Serializer for a PurchaseOrderExtraLine object """
|
||||
"""Serializer for a PurchaseOrderExtraLine object."""
|
||||
|
||||
order_detail = PurchaseOrderSerializer(source='order', many=False, read_only=True)
|
||||
|
||||
class Meta(AbstractExtraLineMeta):
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.PurchaseOrderExtraLine
|
||||
|
||||
|
||||
class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer for receiving a single purchase order line item against a purchase order
|
||||
"""
|
||||
"""A serializer for receiving a single purchase order line item against a purchase order."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [
|
||||
'barcode',
|
||||
'line_item',
|
||||
@ -407,7 +406,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_line_item(self, item):
|
||||
|
||||
"""Validation for the 'line_item' field"""
|
||||
if item.order != self.context['order']:
|
||||
raise ValidationError(_('Line item does not match purchase order'))
|
||||
|
||||
@ -430,7 +429,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
|
||||
"""Validation for the 'quantity' field"""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_("Quantity must be greater than zero"))
|
||||
|
||||
@ -468,10 +467,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_barcode(self, barcode):
|
||||
"""
|
||||
Cannot check in a LineItem with a barcode that is already assigned
|
||||
"""
|
||||
|
||||
"""Cannot check in a LineItem with a barcode that is already assigned."""
|
||||
# Ignore empty barcode values
|
||||
if not barcode or barcode.strip() == '':
|
||||
return None
|
||||
@ -482,7 +478,11 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
return barcode
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
|
||||
- Integer quantity must be provided for serialized stock
|
||||
- Validate serial numbers (if provided)
|
||||
"""
|
||||
data = super().validate(data)
|
||||
|
||||
line_item = data['line_item']
|
||||
@ -513,9 +513,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for receiving items against a purchase order
|
||||
"""
|
||||
"""Serializer for receiving items against a purchase order."""
|
||||
|
||||
items = PurchaseOrderLineItemReceiveSerializer(many=True)
|
||||
|
||||
@ -528,7 +526,11 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
|
||||
- Ensure line items are provided
|
||||
- Check that a location is specified
|
||||
"""
|
||||
super().validate(data)
|
||||
|
||||
items = data.get('items', [])
|
||||
@ -571,10 +573,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Perform the actual database transaction to receive purchase order items
|
||||
"""
|
||||
|
||||
"""Perform the actual database transaction to receive purchase order items."""
|
||||
data = self.validated_data
|
||||
|
||||
request = self.context['request']
|
||||
@ -606,6 +605,8 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [
|
||||
'items',
|
||||
'location',
|
||||
@ -613,11 +614,11 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializers for the PurchaseOrderAttachment model
|
||||
"""
|
||||
"""Serializers for the PurchaseOrderAttachment model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.PurchaseOrderAttachment
|
||||
|
||||
fields = [
|
||||
@ -636,12 +637,10 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
|
||||
|
||||
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrder object
|
||||
"""
|
||||
"""Serializers for the SalesOrder object."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
"""Initialization routine for the serializer"""
|
||||
customer_detail = kwargs.pop('customer_detail', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -651,13 +650,11 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add extra information to the queryset
|
||||
"""Add extra information to the queryset.
|
||||
|
||||
- Number of line items in the SalesOrder
|
||||
- Overdue status of the SalesOrder
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
line_items=SubqueryCount('lines')
|
||||
)
|
||||
@ -684,6 +681,8 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
|
||||
reference = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrder
|
||||
|
||||
fields = [
|
||||
@ -715,8 +714,8 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
|
||||
|
||||
|
||||
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for the SalesOrderAllocation model.
|
||||
"""Serializer for the SalesOrderAllocation model.
|
||||
|
||||
This includes some fields from the related model objects.
|
||||
"""
|
||||
|
||||
@ -736,7 +735,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
shipment_date = serializers.DateField(source='shipment.shipment_date', read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
"""Initialization routine for the serializer"""
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
item_detail = kwargs.pop('item_detail', False)
|
||||
@ -761,6 +760,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
self.fields.pop('customer_detail')
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderAllocation
|
||||
|
||||
fields = [
|
||||
@ -783,16 +784,14 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for a SalesOrderLineItem object """
|
||||
"""Serializer for a SalesOrderLineItem object."""
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add some extra annotations to this queryset:
|
||||
"""Add some extra annotations to this queryset:
|
||||
|
||||
- "Overdue" status (boolean field)
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
overdue=Case(
|
||||
When(
|
||||
@ -803,7 +802,10 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializion routine for the serializer:
|
||||
|
||||
- Add extra related serializer information if required
|
||||
"""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
allocations = kwargs.pop('allocations', False)
|
||||
@ -843,6 +845,8 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderLineItem
|
||||
|
||||
fields = [
|
||||
@ -866,15 +870,15 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for the SalesOrderShipment class
|
||||
"""
|
||||
"""Serializer for the SalesOrderShipment class."""
|
||||
|
||||
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
||||
|
||||
order_detail = SalesOrderSerializer(source='order', read_only=True, many=False)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderShipment
|
||||
|
||||
fields = [
|
||||
@ -893,11 +897,11 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for completing (shipping) a SalesOrderShipment
|
||||
"""
|
||||
"""Serializer for completing (shipping) a SalesOrderShipment."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderShipment
|
||||
|
||||
fields = [
|
||||
@ -908,7 +912,10 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
|
||||
- Ensure the shipment reference is provided
|
||||
"""
|
||||
data = super().validate(data)
|
||||
|
||||
shipment = self.context.get('shipment', None)
|
||||
@ -921,7 +928,7 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Save the serializer to complete the SalesOrderShipment"""
|
||||
shipment = self.context.get('shipment', None)
|
||||
|
||||
if not shipment:
|
||||
@ -945,11 +952,11 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer for allocating a single stock-item against a SalesOrder shipment
|
||||
"""
|
||||
"""A serializer for allocating a single stock-item against a SalesOrder shipment."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [
|
||||
'line_item',
|
||||
'stock_item',
|
||||
@ -965,7 +972,10 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_line_item(self, line_item):
|
||||
"""Custom validation for the 'line_item' field:
|
||||
|
||||
- Ensure the line_item is associated with the particular SalesOrder
|
||||
"""
|
||||
order = self.context['order']
|
||||
|
||||
# Ensure that the line item points to the correct order
|
||||
@ -990,14 +1000,18 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
|
||||
"""Custom validation for the 'quantity' field"""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_("Quantity must be positive"))
|
||||
|
||||
return quantity
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for the serializer:
|
||||
|
||||
- Ensure that the quantity is 1 for serialized stock
|
||||
- Quantity cannot exceed the available amount
|
||||
"""
|
||||
data = super().validate(data)
|
||||
|
||||
stock_item = data['stock_item']
|
||||
@ -1019,12 +1033,10 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for manually marking a sales order as complete
|
||||
"""
|
||||
"""DRF serializer for manually marking a sales order as complete."""
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
"""Custom validation for the serializer"""
|
||||
data = super().validate(data)
|
||||
|
||||
order = self.context['order']
|
||||
@ -1034,7 +1046,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Save the serializer to complete the SalesOrder"""
|
||||
request = self.context['request']
|
||||
order = self.context['order']
|
||||
|
||||
@ -1044,11 +1056,10 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderCancelSerializer(serializers.Serializer):
|
||||
""" Serializer for marking a SalesOrder as cancelled
|
||||
"""
|
||||
"""Serializer for marking a SalesOrder as cancelled."""
|
||||
|
||||
def get_context_data(self):
|
||||
|
||||
"""Add extra context data to the serializer"""
|
||||
order = self.context['order']
|
||||
|
||||
return {
|
||||
@ -1056,18 +1067,18 @@ class SalesOrderCancelSerializer(serializers.Serializer):
|
||||
}
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Save the serializer to cancel the order"""
|
||||
order = self.context['order']
|
||||
|
||||
order.cancel_order()
|
||||
|
||||
|
||||
class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for allocation of serial numbers against a sales order / shipment
|
||||
"""
|
||||
"""DRF serializer for allocation of serial numbers against a sales order / shipment."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [
|
||||
'line_item',
|
||||
'quantity',
|
||||
@ -1084,10 +1095,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_line_item(self, line_item):
|
||||
"""
|
||||
Ensure that the line_item is valid
|
||||
"""
|
||||
|
||||
"""Ensure that the line_item is valid."""
|
||||
order = self.context['order']
|
||||
|
||||
# Ensure that the line item points to the correct order
|
||||
@ -1119,13 +1127,11 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_shipment(self, shipment):
|
||||
"""
|
||||
Validate the shipment:
|
||||
"""Validate the shipment:
|
||||
|
||||
- Must point to the same order
|
||||
- Must not be shipped
|
||||
"""
|
||||
|
||||
order = self.context['order']
|
||||
|
||||
if shipment.shipment_date is not None:
|
||||
@ -1137,14 +1143,12 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
return shipment
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Validation for the serializer:
|
||||
"""Validation for the serializer:
|
||||
|
||||
- Ensure the serial_numbers and quantity fields match
|
||||
- Check that all serial numbers exist
|
||||
- Check that the serial numbers are not yet allocated
|
||||
"""
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
line_item = data['line_item']
|
||||
@ -1207,7 +1211,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Allocate stock items against the sales order"""
|
||||
data = self.validated_data
|
||||
|
||||
line_item = data['line_item']
|
||||
@ -1226,11 +1230,11 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for allocation of stock items against a sales order / shipment
|
||||
"""
|
||||
"""DRF serializer for allocation of stock items against a sales order / shipment."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [
|
||||
'items',
|
||||
'shipment',
|
||||
@ -1247,10 +1251,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_shipment(self, shipment):
|
||||
"""
|
||||
Run validation against the provided shipment instance
|
||||
"""
|
||||
|
||||
"""Run validation against the provided shipment instance."""
|
||||
order = self.context['order']
|
||||
|
||||
if shipment.shipment_date is not None:
|
||||
@ -1262,10 +1263,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
return shipment
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Serializer validation
|
||||
"""
|
||||
|
||||
"""Serializer validation."""
|
||||
data = super().validate(data)
|
||||
|
||||
# Extract SalesOrder from serializer context
|
||||
@ -1279,10 +1277,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Perform the allocation of items against this order
|
||||
"""
|
||||
|
||||
"""Perform the allocation of items against this order."""
|
||||
data = self.validated_data
|
||||
|
||||
items = data['items']
|
||||
@ -1304,20 +1299,22 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
||||
""" Serializer for a SalesOrderExtraLine object """
|
||||
"""Serializer for a SalesOrderExtraLine object."""
|
||||
|
||||
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
||||
|
||||
class Meta(AbstractExtraLineMeta):
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderExtraLine
|
||||
|
||||
|
||||
class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrderAttachment model
|
||||
"""
|
||||
"""Serializers for the SalesOrderAttachment model."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = order.models.SalesOrderAttachment
|
||||
|
||||
fields = [
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Tests for the Order API
|
||||
"""
|
||||
"""Tests for the Order API."""
|
||||
|
||||
import io
|
||||
from datetime import datetime, timedelta
|
||||
@ -18,7 +16,7 @@ from stock.models import StockItem
|
||||
|
||||
|
||||
class OrderTest(InvenTreeAPITestCase):
|
||||
|
||||
"""Base class for order API unit testing"""
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
@ -35,14 +33,8 @@ class OrderTest(InvenTreeAPITestCase):
|
||||
'sales_order.change',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def filter(self, filters, count):
|
||||
"""
|
||||
Test API filters
|
||||
"""
|
||||
|
||||
"""Test API filters."""
|
||||
response = self.get(
|
||||
self.LIST_URL,
|
||||
filters
|
||||
@ -55,14 +47,12 @@ class OrderTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PurchaseOrderTest(OrderTest):
|
||||
"""
|
||||
Tests for the PurchaseOrder API
|
||||
"""
|
||||
"""Tests for the PurchaseOrder API."""
|
||||
|
||||
LIST_URL = reverse('api-po-list')
|
||||
|
||||
def test_po_list(self):
|
||||
|
||||
"""Test the PurchaseOrder list API endpoint"""
|
||||
# List *ALL* PurchaseOrder items
|
||||
self.filter({}, 7)
|
||||
|
||||
@ -79,10 +69,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.filter({'status': 40}, 1)
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Test "overdue" status
|
||||
"""
|
||||
|
||||
"""Test "overdue" status."""
|
||||
self.filter({'overdue': True}, 0)
|
||||
self.filter({'overdue': False}, 7)
|
||||
|
||||
@ -94,7 +81,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.filter({'overdue': False}, 6)
|
||||
|
||||
def test_po_detail(self):
|
||||
|
||||
"""Test the PurchaseOrder detail API endpoint"""
|
||||
url = '/api/order/po/1/'
|
||||
|
||||
response = self.get(url)
|
||||
@ -107,7 +94,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(data['description'], 'Ordering some screws')
|
||||
|
||||
def test_po_reference(self):
|
||||
"""test that a reference with a too big / small reference is not possible"""
|
||||
"""Test that a reference with a too big / small reference is not possible."""
|
||||
# get permissions
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
@ -125,7 +112,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_po_attachments(self):
|
||||
|
||||
"""Test the list endpoint for the PurchaseOrderAttachment model"""
|
||||
url = reverse('api-po-attachment-list')
|
||||
|
||||
response = self.get(url)
|
||||
@ -133,10 +120,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_po_operations(self):
|
||||
"""
|
||||
Test that we can create / edit and delete a PurchaseOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create / edit and delete a PurchaseOrder via the API."""
|
||||
n = models.PurchaseOrder.objects.count()
|
||||
|
||||
url = reverse('api-po-list')
|
||||
@ -223,10 +207,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
response = self.get(url, expected_code=404)
|
||||
|
||||
def test_po_create(self):
|
||||
"""
|
||||
Test that we can create a new PurchaseOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create a new PurchaseOrder via the API."""
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
self.post(
|
||||
@ -240,10 +221,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_po_cancel(self):
|
||||
"""
|
||||
Test the PurchaseOrderCancel API endpoint
|
||||
"""
|
||||
|
||||
"""Test the PurchaseOrderCancel API endpoint."""
|
||||
po = models.PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(po.status, PurchaseOrderStatus.PENDING)
|
||||
@ -269,8 +247,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.post(url, {}, expected_code=400)
|
||||
|
||||
def test_po_complete(self):
|
||||
""" Test the PurchaseOrderComplete API endpoint """
|
||||
|
||||
"""Test the PurchaseOrderComplete API endpoint."""
|
||||
po = models.PurchaseOrder.objects.get(pk=3)
|
||||
|
||||
url = reverse('api-po-complete', kwargs={'pk': po.pk})
|
||||
@ -289,8 +266,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(po.status, PurchaseOrderStatus.COMPLETE)
|
||||
|
||||
def test_po_issue(self):
|
||||
""" Test the PurchaseOrderIssue API endpoint """
|
||||
|
||||
"""Test the PurchaseOrderIssue API endpoint."""
|
||||
po = models.PurchaseOrder.objects.get(pk=2)
|
||||
|
||||
url = reverse('api-po-issue', kwargs={'pk': po.pk})
|
||||
@ -307,6 +283,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(po.status, PurchaseOrderStatus.PLACED)
|
||||
|
||||
def test_po_metadata(self):
|
||||
"""Test the 'metadata' endpoint for the PurchaseOrder model"""
|
||||
url = reverse('api-po-metadata', kwargs={'pk': 1})
|
||||
|
||||
self.patch(
|
||||
@ -324,7 +301,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
|
||||
class PurchaseOrderDownloadTest(OrderTest):
|
||||
"""Unit tests for downloading PurchaseOrder data via the API endpoint"""
|
||||
"""Unit tests for downloading PurchaseOrder data via the API endpoint."""
|
||||
|
||||
required_cols = [
|
||||
'id',
|
||||
@ -342,8 +319,7 @@ class PurchaseOrderDownloadTest(OrderTest):
|
||||
]
|
||||
|
||||
def test_download_wrong_format(self):
|
||||
"""Incorrect format should default raise an error"""
|
||||
|
||||
"""Incorrect format should default raise an error."""
|
||||
url = reverse('api-po-list')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
@ -355,8 +331,7 @@ class PurchaseOrderDownloadTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_download_csv(self):
|
||||
"""Download PurchaseOrder data as .csv"""
|
||||
|
||||
"""Download PurchaseOrder data as .csv."""
|
||||
with self.download_file(
|
||||
reverse('api-po-list'),
|
||||
{
|
||||
@ -380,7 +355,7 @@ class PurchaseOrderDownloadTest(OrderTest):
|
||||
self.assertEqual(order.reference, row['reference'])
|
||||
|
||||
def test_download_line_items(self):
|
||||
|
||||
"""Test that the PurchaseOrderLineItems can be downloaded to a file"""
|
||||
with self.download_file(
|
||||
reverse('api-po-line-list'),
|
||||
{
|
||||
@ -395,11 +370,10 @@ class PurchaseOrderDownloadTest(OrderTest):
|
||||
|
||||
|
||||
class PurchaseOrderReceiveTest(OrderTest):
|
||||
"""
|
||||
Unit tests for receiving items against a PurchaseOrder
|
||||
"""
|
||||
"""Unit tests for receiving items against a PurchaseOrder."""
|
||||
|
||||
def setUp(self):
|
||||
"""Init routines for this unit test class"""
|
||||
super().setUp()
|
||||
|
||||
self.assignRole('purchase_order.add')
|
||||
@ -415,10 +389,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
order.save()
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
Test without any POST data
|
||||
"""
|
||||
|
||||
"""Test without any POST data."""
|
||||
data = self.post(self.url, {}, expected_code=400).data
|
||||
|
||||
self.assertIn('This field is required', str(data['items']))
|
||||
@ -428,10 +399,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_no_items(self):
|
||||
"""
|
||||
Test with an empty list of items
|
||||
"""
|
||||
|
||||
"""Test with an empty list of items."""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -447,10 +415,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_invalid_items(self):
|
||||
"""
|
||||
Test than errors are returned as expected for invalid data
|
||||
"""
|
||||
|
||||
"""Test than errors are returned as expected for invalid data."""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -473,10 +438,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_invalid_status(self):
|
||||
"""
|
||||
Test with an invalid StockStatus value
|
||||
"""
|
||||
|
||||
"""Test with an invalid StockStatus value."""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -498,10 +460,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_mismatched_items(self):
|
||||
"""
|
||||
Test for supplier parts which *do* exist but do not match the order supplier
|
||||
"""
|
||||
|
||||
"""Test for supplier parts which *do* exist but do not match the order supplier."""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -523,10 +482,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_null_barcode(self):
|
||||
"""
|
||||
Test than a "null" barcode field can be provided
|
||||
"""
|
||||
|
||||
"""Test than a "null" barcode field can be provided."""
|
||||
# Set stock item barcode
|
||||
item = StockItem.objects.get(pk=1)
|
||||
item.save()
|
||||
@ -548,13 +504,11 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_invalid_barcodes(self):
|
||||
"""
|
||||
Tests for checking in items with invalid barcodes:
|
||||
"""Tests for checking in items with invalid barcodes:
|
||||
|
||||
- Cannot check in "duplicate" barcodes
|
||||
- Barcodes cannot match UID field for existing StockItem
|
||||
"""
|
||||
|
||||
# Set stock item barcode
|
||||
item = StockItem.objects.get(pk=1)
|
||||
item.uid = 'MY-BARCODE-HASH'
|
||||
@ -603,10 +557,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_valid(self):
|
||||
"""
|
||||
Test receipt of valid data
|
||||
"""
|
||||
|
||||
"""Test receipt of valid data."""
|
||||
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||
|
||||
@ -683,10 +634,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
|
||||
|
||||
def test_batch_code(self):
|
||||
"""
|
||||
Test that we can supply a 'batch code' when receiving items
|
||||
"""
|
||||
|
||||
"""Test that we can supply a 'batch code' when receiving items."""
|
||||
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||
|
||||
@ -727,10 +675,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(item_2.batch, 'xyz-789')
|
||||
|
||||
def test_serial_numbers(self):
|
||||
"""
|
||||
Test that we can supply a 'serial number' when receiving items
|
||||
"""
|
||||
|
||||
"""Test that we can supply a 'serial number' when receiving items."""
|
||||
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||
|
||||
@ -786,14 +731,12 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
|
||||
|
||||
class SalesOrderTest(OrderTest):
|
||||
"""
|
||||
Tests for the SalesOrder API
|
||||
"""
|
||||
"""Tests for the SalesOrder API."""
|
||||
|
||||
LIST_URL = reverse('api-so-list')
|
||||
|
||||
def test_so_list(self):
|
||||
|
||||
"""Test the SalesOrder list API endpoint"""
|
||||
# All orders
|
||||
self.filter({}, 5)
|
||||
|
||||
@ -811,10 +754,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.filter({'status': 99}, 0) # Invalid
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Test "overdue" status
|
||||
"""
|
||||
|
||||
"""Test "overdue" status."""
|
||||
self.filter({'overdue': True}, 0)
|
||||
self.filter({'overdue': False}, 5)
|
||||
|
||||
@ -827,7 +767,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.filter({'overdue': False}, 3)
|
||||
|
||||
def test_so_detail(self):
|
||||
|
||||
"""Test the SalesOrder detail endpoint"""
|
||||
url = '/api/order/so/1/'
|
||||
|
||||
response = self.get(url)
|
||||
@ -837,16 +777,13 @@ class SalesOrderTest(OrderTest):
|
||||
self.assertEqual(data['pk'], 1)
|
||||
|
||||
def test_so_attachments(self):
|
||||
|
||||
"""Test the list endpoint for the SalesOrderAttachment model"""
|
||||
url = reverse('api-so-attachment-list')
|
||||
|
||||
self.get(url)
|
||||
|
||||
def test_so_operations(self):
|
||||
"""
|
||||
Test that we can create / edit and delete a SalesOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create / edit and delete a SalesOrder via the API."""
|
||||
n = models.SalesOrder.objects.count()
|
||||
|
||||
url = reverse('api-so-list')
|
||||
@ -926,10 +863,7 @@ class SalesOrderTest(OrderTest):
|
||||
response = self.get(url, expected_code=404)
|
||||
|
||||
def test_so_create(self):
|
||||
"""
|
||||
Test that we can create a new SalesOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create a new SalesOrder via the API."""
|
||||
self.assignRole('sales_order.add')
|
||||
|
||||
self.post(
|
||||
@ -943,8 +877,7 @@ class SalesOrderTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_so_cancel(self):
|
||||
""" Test API endpoint for cancelling a SalesOrder """
|
||||
|
||||
"""Test API endpoint for cancelling a SalesOrder."""
|
||||
so = models.SalesOrder.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(so.status, SalesOrderStatus.PENDING)
|
||||
@ -963,6 +896,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.assertEqual(so.status, SalesOrderStatus.CANCELLED)
|
||||
|
||||
def test_so_metadata(self):
|
||||
"""Test the 'metadata' API endpoint for the SalesOrder model"""
|
||||
url = reverse('api-so-metadata', kwargs={'pk': 1})
|
||||
|
||||
self.patch(
|
||||
@ -980,12 +914,10 @@ class SalesOrderTest(OrderTest):
|
||||
|
||||
|
||||
class SalesOrderLineItemTest(OrderTest):
|
||||
"""
|
||||
Tests for the SalesOrderLineItem API
|
||||
"""
|
||||
"""Tests for the SalesOrderLineItem API."""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
"""Init routine for this unit test class"""
|
||||
super().setUp()
|
||||
|
||||
# List of salable parts
|
||||
@ -1005,9 +937,7 @@ class SalesOrderLineItemTest(OrderTest):
|
||||
self.url = reverse('api-so-line-list')
|
||||
|
||||
def test_so_line_list(self):
|
||||
|
||||
# List *all* lines
|
||||
|
||||
"""Test list endpoint"""
|
||||
response = self.get(
|
||||
self.url,
|
||||
{},
|
||||
@ -1060,17 +990,17 @@ class SalesOrderLineItemTest(OrderTest):
|
||||
|
||||
|
||||
class SalesOrderDownloadTest(OrderTest):
|
||||
"""Unit tests for downloading SalesOrder data via the API endpoint"""
|
||||
"""Unit tests for downloading SalesOrder data via the API endpoint."""
|
||||
|
||||
def test_download_fail(self):
|
||||
"""Test that downloading without the 'export' option fails"""
|
||||
|
||||
"""Test that downloading without the 'export' option fails."""
|
||||
url = reverse('api-so-list')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.download_file(url, {}, expected_code=200)
|
||||
|
||||
def test_download_xls(self):
|
||||
"""Test xls file download"""
|
||||
url = reverse('api-so-list')
|
||||
|
||||
# Download .xls file
|
||||
@ -1086,7 +1016,7 @@ class SalesOrderDownloadTest(OrderTest):
|
||||
self.assertTrue(isinstance(fo, io.BytesIO))
|
||||
|
||||
def test_download_csv(self):
|
||||
|
||||
"""Tesst that the list of sales orders can be downloaded as a .csv file"""
|
||||
url = reverse('api-so-list')
|
||||
|
||||
required_cols = [
|
||||
@ -1151,11 +1081,10 @@ class SalesOrderDownloadTest(OrderTest):
|
||||
|
||||
|
||||
class SalesOrderAllocateTest(OrderTest):
|
||||
"""
|
||||
Unit tests for allocating stock items against a SalesOrder
|
||||
"""
|
||||
"""Unit tests for allocating stock items against a SalesOrder."""
|
||||
|
||||
def setUp(self):
|
||||
"""Init routines for this unit testing class"""
|
||||
super().setUp()
|
||||
|
||||
self.assignRole('sales_order.add')
|
||||
@ -1188,10 +1117,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_invalid(self):
|
||||
"""
|
||||
Test POST with invalid data
|
||||
"""
|
||||
|
||||
"""Test POST with invalid data."""
|
||||
# No data
|
||||
response = self.post(self.url, {}, expected_code=400)
|
||||
|
||||
@ -1244,11 +1170,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
self.assertIn('Shipment is not associated with this order', str(response.data['shipment']))
|
||||
|
||||
def test_allocate(self):
|
||||
"""
|
||||
Test the the allocation endpoint acts as expected,
|
||||
when provided with valid data!
|
||||
"""
|
||||
|
||||
"""Test the the allocation endpoint acts as expected, when provided with valid data!"""
|
||||
# First, check that there are no line items allocated against this SalesOrder
|
||||
self.assertEqual(self.order.stock_allocations.count(), 0)
|
||||
|
||||
@ -1278,8 +1200,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
self.assertEqual(line.allocations.count(), 1)
|
||||
|
||||
def test_shipment_complete(self):
|
||||
"""Test that we can complete a shipment via the API"""
|
||||
|
||||
"""Test that we can complete a shipment via the API."""
|
||||
url = reverse('api-so-shipment-ship', kwargs={'pk': self.shipment.pk})
|
||||
|
||||
self.assertFalse(self.shipment.is_complete())
|
||||
@ -1340,7 +1261,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
self.assertEqual(self.shipment.link, 'http://test.com/link.html')
|
||||
|
||||
def test_sales_order_shipment_list(self):
|
||||
|
||||
"""Test the SalesOrderShipment list API endpoint"""
|
||||
url = reverse('api-so-shipment-list')
|
||||
|
||||
# Create some new shipments via the API
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Unit tests for the 'order' model data migrations
|
||||
"""
|
||||
"""Unit tests for the 'order' model data migrations."""
|
||||
|
||||
from django_test_migrations.contrib.unittest_case import MigratorTestCase
|
||||
|
||||
@ -8,18 +6,13 @@ from InvenTree.status_codes import SalesOrderStatus
|
||||
|
||||
|
||||
class TestRefIntMigrations(MigratorTestCase):
|
||||
"""
|
||||
Test entire schema migration
|
||||
"""
|
||||
"""Test entire schema migration."""
|
||||
|
||||
migrate_from = ('order', '0040_salesorder_target_date')
|
||||
migrate_to = ('order', '0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create initial data set
|
||||
"""
|
||||
|
||||
"""Create initial data set."""
|
||||
# Create a purchase order from a supplier
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
|
||||
@ -57,10 +50,7 @@ class TestRefIntMigrations(MigratorTestCase):
|
||||
print(sales_order.reference_int)
|
||||
|
||||
def test_ref_field(self):
|
||||
"""
|
||||
Test that the 'reference_int' field has been created and is filled out correctly
|
||||
"""
|
||||
|
||||
"""Test that the 'reference_int' field has been created and is filled out correctly."""
|
||||
PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
|
||||
SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
|
||||
|
||||
@ -75,18 +65,13 @@ class TestRefIntMigrations(MigratorTestCase):
|
||||
|
||||
|
||||
class TestShipmentMigration(MigratorTestCase):
|
||||
"""
|
||||
Test data migration for the "SalesOrderShipment" model
|
||||
"""
|
||||
"""Test data migration for the "SalesOrderShipment" model."""
|
||||
|
||||
migrate_from = ('order', '0051_auto_20211014_0623')
|
||||
migrate_to = ('order', '0055_auto_20211025_0645')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create an initial SalesOrder
|
||||
"""
|
||||
|
||||
"""Create an initial SalesOrder."""
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
|
||||
customer = Company.objects.create(
|
||||
@ -112,10 +97,7 @@ class TestShipmentMigration(MigratorTestCase):
|
||||
self.old_state.apps.get_model('order', 'salesordershipment')
|
||||
|
||||
def test_shipment_creation(self):
|
||||
"""
|
||||
Check that a SalesOrderShipment has been created
|
||||
"""
|
||||
|
||||
"""Check that a SalesOrderShipment has been created."""
|
||||
SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
|
||||
Shipment = self.new_state.apps.get_model('order', 'salesordershipment')
|
||||
|
||||
@ -125,18 +107,13 @@ class TestShipmentMigration(MigratorTestCase):
|
||||
|
||||
|
||||
class TestAdditionalLineMigration(MigratorTestCase):
|
||||
"""
|
||||
Test entire schema migration
|
||||
"""
|
||||
"""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 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')
|
||||
@ -199,10 +176,7 @@ class TestAdditionalLineMigration(MigratorTestCase):
|
||||
# )
|
||||
|
||||
def test_po_migration(self):
|
||||
"""
|
||||
Test that the the PO lines where converted correctly
|
||||
"""
|
||||
|
||||
"""Test that the the PO lines where converted correctly."""
|
||||
PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
|
||||
for ii in range(10):
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests for the SalesOrder models"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@ -15,13 +15,10 @@ from stock.models import StockItem
|
||||
|
||||
|
||||
class SalesOrderTest(TestCase):
|
||||
"""
|
||||
Run tests to ensure that the SalesOrder model is working correctly.
|
||||
|
||||
"""
|
||||
"""Run tests to ensure that the SalesOrder model is working correctly."""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
"""Initial setup for this set of unit tests"""
|
||||
# Create a Company to ship the goods to
|
||||
self.customer = Company.objects.create(name="ABC Co", description="My customer", is_customer=True)
|
||||
|
||||
@ -48,11 +45,21 @@ class SalesOrderTest(TestCase):
|
||||
# Create a line item
|
||||
self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part)
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Tests for overdue functionality
|
||||
"""
|
||||
def test_rebuild_reference(self):
|
||||
"""Test that the 'reference_int' field gets rebuilt when the model is saved"""
|
||||
|
||||
self.assertEqual(self.order.reference_int, 1234)
|
||||
|
||||
self.order.reference = '999'
|
||||
self.order.save()
|
||||
self.assertEqual(self.order.reference_int, 999)
|
||||
|
||||
self.order.reference = '1000K'
|
||||
self.order.save()
|
||||
self.assertEqual(self.order.reference_int, 1000)
|
||||
|
||||
def test_overdue(self):
|
||||
"""Tests for overdue functionality."""
|
||||
today = datetime.now().date()
|
||||
|
||||
# By default, order is *not* overdue as the target date is not set
|
||||
@ -69,6 +76,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertFalse(self.order.is_overdue)
|
||||
|
||||
def test_empty_order(self):
|
||||
"""Test for an empty order"""
|
||||
self.assertEqual(self.line.quantity, 50)
|
||||
self.assertEqual(self.line.allocated_quantity(), 0)
|
||||
self.assertEqual(self.line.fulfilled_quantity(), 0)
|
||||
@ -79,14 +87,13 @@ class SalesOrderTest(TestCase):
|
||||
self.assertFalse(self.order.is_fully_allocated())
|
||||
|
||||
def test_add_duplicate_line_item(self):
|
||||
# Adding a duplicate line item to a SalesOrder is accepted
|
||||
"""Adding a duplicate line item to a SalesOrder is accepted"""
|
||||
|
||||
for ii in range(1, 5):
|
||||
SalesOrderLineItem.objects.create(order=self.order, part=self.part, quantity=ii)
|
||||
|
||||
def allocate_stock(self, full=True):
|
||||
|
||||
# Allocate stock to the order
|
||||
"""Allocate stock to the order"""
|
||||
SalesOrderAllocation.objects.create(
|
||||
line=self.line,
|
||||
shipment=self.shipment,
|
||||
@ -101,7 +108,7 @@ class SalesOrderTest(TestCase):
|
||||
)
|
||||
|
||||
def test_allocate_partial(self):
|
||||
# Partially allocate stock
|
||||
"""Partially allocate stock"""
|
||||
self.allocate_stock(False)
|
||||
|
||||
self.assertFalse(self.order.is_fully_allocated())
|
||||
@ -110,7 +117,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(self.line.fulfilled_quantity(), 0)
|
||||
|
||||
def test_allocate_full(self):
|
||||
# Fully allocate stock
|
||||
"""Fully allocate stock"""
|
||||
self.allocate_stock(True)
|
||||
|
||||
self.assertTrue(self.order.is_fully_allocated())
|
||||
@ -118,8 +125,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(self.line.allocated_quantity(), 50)
|
||||
|
||||
def test_order_cancel(self):
|
||||
# Allocate line items then cancel the order
|
||||
|
||||
"""Allocate line items then cancel the order"""
|
||||
self.allocate_stock(True)
|
||||
|
||||
self.assertEqual(SalesOrderAllocation.objects.count(), 2)
|
||||
@ -137,8 +143,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_complete_order(self):
|
||||
# Allocate line items, then ship the order
|
||||
|
||||
"""Allocate line items, then ship the order"""
|
||||
# Assert some stuff before we run the test
|
||||
# Initially there are two stock items
|
||||
self.assertEqual(StockItem.objects.count(), 2)
|
||||
@ -199,8 +204,7 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(self.line.allocated_quantity(), 50)
|
||||
|
||||
def test_default_shipment(self):
|
||||
# Test sales order default shipment creation
|
||||
|
||||
"""Test sales order default shipment creation"""
|
||||
# Default setting value should be False
|
||||
self.assertEqual(False, InvenTreeSetting.get_setting('SALESORDER_DEFAULT_SHIPMENT'))
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
""" Unit tests for Order views (see views.py) """
|
||||
"""Unit tests for Order views (see views.py)"""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -6,7 +6,7 @@ from InvenTree.helpers import InvenTreeTestCase
|
||||
|
||||
|
||||
class OrderViewTestCase(InvenTreeTestCase):
|
||||
|
||||
"""Base unit test class for order views"""
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
@ -29,26 +29,26 @@ class OrderViewTestCase(InvenTreeTestCase):
|
||||
|
||||
|
||||
class OrderListTest(OrderViewTestCase):
|
||||
|
||||
"""Unit tests for the PurchaseOrder index page"""
|
||||
def test_order_list(self):
|
||||
"""Tests for the PurchaseOrder index page"""
|
||||
response = self.client.get(reverse('po-index'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class PurchaseOrderTests(OrderViewTestCase):
|
||||
""" Tests for PurchaseOrder views """
|
||||
"""Tests for PurchaseOrder views."""
|
||||
|
||||
def test_detail_view(self):
|
||||
""" Retrieve PO detail view """
|
||||
"""Retrieve PO detail view."""
|
||||
response = self.client.get(reverse('po-detail', args=(1,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
keys = response.context.keys()
|
||||
self.assertIn('PurchaseOrderStatus', keys)
|
||||
|
||||
def test_po_export(self):
|
||||
""" Export PurchaseOrder """
|
||||
|
||||
"""Export PurchaseOrder."""
|
||||
response = self.client.get(reverse('po-export', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Response should be streaming-content (file download)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Various unit tests for order models"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@ -14,9 +14,7 @@ from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
|
||||
|
||||
class OrderTest(TestCase):
|
||||
"""
|
||||
Tests to ensure that the order models are functioning correctly.
|
||||
"""
|
||||
"""Tests to ensure that the order models are functioning correctly."""
|
||||
|
||||
fixtures = [
|
||||
'company',
|
||||
@ -30,8 +28,7 @@ class OrderTest(TestCase):
|
||||
]
|
||||
|
||||
def test_basics(self):
|
||||
""" Basic tests e.g. repr functions etc """
|
||||
|
||||
"""Basic tests e.g. repr functions etc."""
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
|
||||
@ -42,11 +39,19 @@ class OrderTest(TestCase):
|
||||
|
||||
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Test overdue status functionality
|
||||
"""
|
||||
def test_rebuild_reference(self):
|
||||
"""Test that the reference_int field is correctly updated when the model is saved"""
|
||||
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
order.save()
|
||||
self.assertEqual(order.reference_int, 1)
|
||||
|
||||
order.reference = '12345XYZ'
|
||||
order.save()
|
||||
self.assertEqual(order.reference_int, 12345)
|
||||
|
||||
def test_overdue(self):
|
||||
"""Test overdue status functionality."""
|
||||
today = datetime.now().date()
|
||||
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
@ -61,8 +66,7 @@ class OrderTest(TestCase):
|
||||
self.assertFalse(order.is_overdue)
|
||||
|
||||
def test_on_order(self):
|
||||
""" There should be 3 separate items on order for the M2x4 LPHS part """
|
||||
|
||||
"""There should be 3 separate items on order for the M2x4 LPHS part."""
|
||||
part = Part.objects.get(name='M2x4 LPHS')
|
||||
|
||||
open_orders = []
|
||||
@ -76,8 +80,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(part.on_order, 1400)
|
||||
|
||||
def test_add_items(self):
|
||||
""" Test functions for adding line items to an order """
|
||||
|
||||
"""Test functions for adding line items to an order."""
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||
@ -113,8 +116,7 @@ class OrderTest(TestCase):
|
||||
order.add_line_item(sku, 99)
|
||||
|
||||
def test_pricing(self):
|
||||
""" Test functions for adding line items to an order including price-breaks """
|
||||
|
||||
"""Test functions for adding line items to an order including price-breaks."""
|
||||
order = PurchaseOrder.objects.get(pk=7)
|
||||
|
||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||
@ -146,8 +148,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(order.lines.first().purchase_price.amount, 1.25)
|
||||
|
||||
def test_receive(self):
|
||||
""" Test order receiving functions """
|
||||
|
||||
"""Test order receiving functions."""
|
||||
part = Part.objects.get(name='M2x4 LPHS')
|
||||
|
||||
# Receive some items
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""
|
||||
URL lookup for the Order app. Provides URL endpoints for:
|
||||
"""URL lookup for the Order app. Provides URL endpoints for:
|
||||
|
||||
- List view of Purchase Orders
|
||||
- Detail view of Purchase Orders
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django views for interacting with Order app
|
||||
"""
|
||||
"""Django views for interacting with Order app."""
|
||||
|
||||
import logging
|
||||
from decimal import Decimal, InvalidOperation
|
||||
@ -33,48 +31,36 @@ logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
""" List view for all purchase orders """
|
||||
"""List view for all purchase orders."""
|
||||
|
||||
model = PurchaseOrder
|
||||
template_name = 'order/purchase_orders.html'
|
||||
context_object_name = 'orders'
|
||||
|
||||
def get_queryset(self):
|
||||
""" Retrieve the list of purchase orders,
|
||||
ensure that the most recent ones are returned first. """
|
||||
|
||||
"""Retrieve the list of purchase orders, ensure that the most recent ones are returned first."""
|
||||
queryset = PurchaseOrder.objects.all().order_by('-creation_date')
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class SalesOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
|
||||
"""SalesOrder index (list) view class"""
|
||||
model = SalesOrder
|
||||
template_name = 'order/sales_orders.html'
|
||||
context_object_name = 'orders'
|
||||
|
||||
|
||||
class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
""" Detail view for a PurchaseOrder object """
|
||||
"""Detail view for a PurchaseOrder object."""
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = PurchaseOrder.objects.all().prefetch_related('lines')
|
||||
template_name = 'order/purchase_order_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
""" Detail view for a SalesOrder object """
|
||||
"""Detail view for a SalesOrder object."""
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = SalesOrder.objects.all().prefetch_related('lines__allocations__item__purchase_order')
|
||||
@ -82,9 +68,10 @@ class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView)
|
||||
|
||||
|
||||
class PurchaseOrderUpload(FileManagementFormView):
|
||||
''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) '''
|
||||
"""PurchaseOrder: Upload file, match to fields and parts (using multi-Step form)"""
|
||||
|
||||
class OrderFileManager(FileManager):
|
||||
"""Specify required fields"""
|
||||
REQUIRED_HEADERS = [
|
||||
'Quantity',
|
||||
]
|
||||
@ -126,13 +113,11 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
file_manager_class = OrderFileManager
|
||||
|
||||
def get_order(self):
|
||||
""" Get order or return 404 """
|
||||
|
||||
"""Get order or return 404."""
|
||||
return get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
||||
|
||||
def get_context_data(self, form, **kwargs):
|
||||
""" Handle context data for order """
|
||||
|
||||
"""Handle context data for order."""
|
||||
context = super().get_context_data(form=form, **kwargs)
|
||||
|
||||
order = self.get_order()
|
||||
@ -142,11 +127,11 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
return context
|
||||
|
||||
def get_field_selection(self):
|
||||
""" Once data columns have been selected, attempt to pre-select the proper data from the database.
|
||||
"""Once data columns have been selected, attempt to pre-select the proper data from the database.
|
||||
|
||||
This function is called once the field selection has been validated.
|
||||
The pre-fill data are then passed through to the SupplierPart selection form.
|
||||
"""
|
||||
|
||||
order = self.get_order()
|
||||
|
||||
self.allowed_items = SupplierPart.objects.filter(supplier=order.supplier).prefetch_related('manufacturer_part')
|
||||
@ -231,8 +216,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
row['notes'] = notes
|
||||
|
||||
def done(self, form_list, **kwargs):
|
||||
""" Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """
|
||||
|
||||
"""Once all the data is in, process it to add PurchaseOrderLineItem instances to the order."""
|
||||
order = self.get_order()
|
||||
items = self.get_clean_items()
|
||||
|
||||
@ -263,8 +247,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
|
||||
|
||||
class SalesOrderExport(AjaxView):
|
||||
"""
|
||||
Export a sales order
|
||||
"""Export a sales order.
|
||||
|
||||
- File format can optionally be passed as a query parameter e.g. ?format=CSV
|
||||
- Default file format is CSV
|
||||
@ -275,7 +258,7 @@ class SalesOrderExport(AjaxView):
|
||||
role_required = 'sales_order.view'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
"""Perform GET request to export SalesOrder dataset"""
|
||||
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
|
||||
|
||||
export_format = request.GET.get('format', 'csv')
|
||||
@ -290,7 +273,7 @@ class SalesOrderExport(AjaxView):
|
||||
|
||||
|
||||
class PurchaseOrderExport(AjaxView):
|
||||
""" File download for a purchase order
|
||||
"""File download for a purchase order.
|
||||
|
||||
- File format can be optionally passed as a query param e.g. ?format=CSV
|
||||
- Default file format is CSV
|
||||
@ -302,7 +285,7 @@ class PurchaseOrderExport(AjaxView):
|
||||
role_required = 'purchase_order.view'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
"""Perform GET request to export PurchaseOrder dataset"""
|
||||
order = get_object_or_404(PurchaseOrder, pk=self.kwargs.get('pk', None))
|
||||
|
||||
export_format = request.GET.get('format', 'csv')
|
||||
@ -321,15 +304,17 @@ class PurchaseOrderExport(AjaxView):
|
||||
|
||||
|
||||
class LineItemPricing(PartPricing):
|
||||
""" View for inspecting part pricing information """
|
||||
"""View for inspecting part pricing information."""
|
||||
|
||||
class EnhancedForm(PartPricing.form_class):
|
||||
"""Extra form options"""
|
||||
pk = IntegerField(widget=HiddenInput())
|
||||
so_line = IntegerField(widget=HiddenInput())
|
||||
|
||||
form_class = EnhancedForm
|
||||
|
||||
def get_part(self, id=False):
|
||||
"""Return the Part instance associated with this view"""
|
||||
if 'line_item' in self.request.GET:
|
||||
try:
|
||||
part_id = self.request.GET.get('line_item')
|
||||
@ -350,6 +335,7 @@ class LineItemPricing(PartPricing):
|
||||
return part
|
||||
|
||||
def get_so(self, pk=False):
|
||||
"""Return the SalesOrderLineItem associated with this view"""
|
||||
so_line = self.request.GET.get('line_item', None)
|
||||
if not so_line:
|
||||
so_line = self.request.POST.get('so_line', None)
|
||||
@ -365,20 +351,21 @@ class LineItemPricing(PartPricing):
|
||||
return None
|
||||
|
||||
def get_quantity(self):
|
||||
""" Return set quantity in decimal format """
|
||||
"""Return set quantity in decimal format."""
|
||||
qty = Decimal(self.request.GET.get('quantity', 1))
|
||||
if qty == 1:
|
||||
return Decimal(self.request.POST.get('quantity', 1))
|
||||
return qty
|
||||
|
||||
def get_initials(self):
|
||||
"""Return initial context values for this view"""
|
||||
initials = super().get_initials()
|
||||
initials['pk'] = self.get_part(id=True)
|
||||
initials['so_line'] = self.get_so(pk=True)
|
||||
return initials
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# parse extra actions
|
||||
"""Respond to a POST request to get particular pricing information"""
|
||||
REF = 'act-btn_'
|
||||
act_btn = [a.replace(REF, '') for a in self.request.POST if REF in a]
|
||||
|
||||
|
Reference in New Issue
Block a user