2
0
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 merge 99676ee

* 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 commit ca96654622.

* use typing here

* Revert "Make API code cleaner"

This reverts commit 24fb68bd3e.

* 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:
Matthias Mair
2022-06-01 17:37:39 +02:00
committed by GitHub
parent 66a6915213
commit 0c97a50e47
223 changed files with 4416 additions and 6980 deletions

View File

@ -1,3 +1 @@
"""
The Order module is responsible for managing Orders
"""
"""The Order module is responsible for managing Orders."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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