docstring adjustments

This commit is contained in:
Matthias 2022-05-28 15:23:57 +02:00
parent 60f13ad2e8
commit 1e90900918
No known key found for this signature in database
GPG Key ID: AB6D0E6C4CB65093
16 changed files with 388 additions and 1005 deletions

View File

@ -1,6 +1,4 @@
"""
Company database model definitions
"""
"""Company database model definitions"""
import os
@ -27,7 +25,7 @@ from InvenTree.status_codes import PurchaseOrderStatus
def rename_company_image(instance, filename):
""" Function to rename a company image after upload
"""Function to rename a company image after upload
Args:
instance: Company object
@ -36,7 +34,6 @@ def rename_company_image(instance, filename):
Returns:
New image filename
"""
base = 'company_images'
if filename.count('.') > 0:
@ -54,6 +51,7 @@ def rename_company_image(instance, filename):
class Company(models.Model):
""" A Company object represents an external company.
It may be a supplier or a customer or a manufacturer (or a combination)
- A supplier is a company from which parts can be purchased
@ -156,7 +154,6 @@ class Company(models.Model):
- If the currency code is invalid, use the default currency
- If the currency code is not specified, use the default currency
"""
code = self.currency
if code not in CURRENCIES:
@ -174,7 +171,6 @@ class Company(models.Model):
def get_image_url(self):
""" Return the URL of the image for this company """
if self.image:
return getMediaUrl(self.image.url)
else:
@ -182,7 +178,6 @@ class Company(models.Model):
def get_thumbnail_url(self):
""" Return the URL for the thumbnail image for this Company """
if self.image:
return getMediaUrl(self.image.thumbnail.url)
else:
@ -247,7 +242,6 @@ class Company(models.Model):
- Failed / lost
- Returned
"""
return self.purchase_orders.exclude(status__in=PurchaseOrderStatus.OPEN)
def complete_purchase_orders(self):
@ -255,7 +249,6 @@ class Company(models.Model):
def failed_purchase_orders(self):
""" Return any purchase orders which were not successful """
return self.purchase_orders.filter(status__in=PurchaseOrderStatus.FAILED)
@ -346,10 +339,7 @@ class ManufacturerPart(models.Model):
@classmethod
def create(cls, part, manufacturer, mpn, description, link=None):
""" Check if ManufacturerPart instance does not already exist
then create it
"""
"""Check if ManufacturerPart instance does not already exist then create it"""
manufacturer_part = None
try:
@ -509,7 +499,6 @@ class SupplierPart(models.Model):
def save(self, *args, **kwargs):
""" Overriding save method to connect an existing ManufacturerPart """
manufacturer_part = None
if all(key in kwargs for key in ('manufacturer', 'MPN')):
@ -593,10 +582,10 @@ class SupplierPart(models.Model):
@property
def manufacturer_string(self):
""" Format a MPN string for this SupplierPart.
"""Format a MPN string for this SupplierPart.
Concatenates manufacture name and part number.
"""
items = []
if self.manufacturer_part:
@ -621,14 +610,12 @@ class SupplierPart(models.Model):
return self.get_price(1)
def add_price_break(self, quantity, price):
"""
Create a new price break for this part
"""Create a new price break for this part
args:
quantity - Numerical quantity
price - Must be a Money object
"""
# Check if a price break at that quantity already exists...
if self.price_breaks.filter(quantity=quantity, part=self.pk).exists():
return
@ -642,18 +629,14 @@ class SupplierPart(models.Model):
get_price = common.models.get_price
def open_orders(self):
""" Return a database query for PurchaseOrder line items for this SupplierPart,
limited to purchase orders that are open / outstanding.
"""
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding."""
return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatus.OPEN)
def on_order(self):
""" Return the total quantity of items currently on order.
"""Return the total quantity of items currently on order.
Subtract partially received stock as appropriate
"""
totals = self.open_orders().aggregate(Sum('quantity'), Sum('received'))
# Quantity on order
@ -668,8 +651,7 @@ class SupplierPart(models.Model):
return max(q - r, 0)
def purchase_orders(self):
""" Returns a list of purchase orders relating to this supplier part """
"""Returns a list of purchase orders relating to this supplier part"""
return [line.order for line in self.purchase_order_line_items.all().prefetch_related('order')]
@property
@ -692,7 +674,7 @@ class SupplierPart(models.Model):
class SupplierPriceBreak(common.models.PriceBreak):
""" Represents a quantity price break for a SupplierPart.
"""Represents a quantity price break for a SupplierPart.
- Suppliers can offer discounts at larger quantities
- SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s)

View File

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

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:
@ -159,15 +149,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 +218,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
@ -247,8 +235,7 @@ class PurchaseOrder(Order):
@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 +246,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
@ -344,7 +330,7 @@ class PurchaseOrder(Order):
@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.
"""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
@ -355,7 +341,6 @@ class PurchaseOrder(Order):
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)
"""
try:
quantity = int(quantity)
if quantity <= 0:
@ -396,8 +381,7 @@ 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 +391,7 @@ 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 +401,17 @@ 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:"""
return self.status in [
PurchaseOrderStatus.PLACED,
PurchaseOrderStatus.PENDING
@ -441,8 +419,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,16 +427,14 @@ 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
@ -477,16 +452,12 @@ class PurchaseOrder(Order):
@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 +544,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
@ -590,8 +560,7 @@ class SalesOrder(Order):
@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 +571,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
@ -682,12 +650,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)
@ -699,17 +665,13 @@ class SalesOrder(Order):
@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 +679,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 +687,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 +721,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 +736,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 +744,11 @@ 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")
- Mark the order as 'cancelled'
- Delete any StockItems which have been allocated
"""
if not self.can_cancel():
return False
@ -820,15 +768,11 @@ class SalesOrder(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
@ -840,16 +784,11 @@ class SalesOrder(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
@ -867,9 +806,7 @@ class SalesOrder(Order):
@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,9 +818,7 @@ 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():
@ -896,9 +831,7 @@ class PurchaseOrderAttachment(InvenTreeAttachment):
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():
@ -911,7 +844,7 @@ class SalesOrderAttachment(InvenTreeAttachment):
class OrderLineItem(models.Model):
""" Abstract model for an order line item
"""Abstract model for an order line item
Attributes:
quantity: Number of items
@ -951,8 +884,8 @@ 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
"""
@ -984,7 +917,7 @@ class OrderExtraLine(OrderLineItem):
class PurchaseOrderLineItem(OrderLineItem):
""" Model for a purchase order line item.
"""Model for a purchase order line item.
Attributes:
order: Reference to a PurchaseOrder object
@ -1024,8 +957,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 +999,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 +1014,13 @@ 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
@ -1105,8 +1034,7 @@ class PurchaseOrderExtraLine(OrderExtraLine):
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
@ -1150,47 +1078,38 @@ class SalesOrderLineItem(OrderLineItem):
]
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
@ -1297,14 +1216,12 @@ class SalesOrderShipment(models.Model):
@transaction.atomic
def complete_shipment(self, user, **kwargs):
"""
Complete this particular shipment:
"""Complete this particular shipment:
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 +1260,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
@ -1358,8 +1275,7 @@ class SalesOrderExtraLine(OrderExtraLine):
class SalesOrderAllocation(models.Model):
"""
This model is used to 'allocate' stock items to a SalesOrder.
"""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.
@ -1368,7 +1284,6 @@ class SalesOrderAllocation(models.Model):
shipment: SalesOrderShipment reference
item: StockItem reference
quantity: Quantity to take from the StockItem
"""
@staticmethod
@ -1376,8 +1291,7 @@ class SalesOrderAllocation(models.Model):
return reverse('api-so-allocation-list')
def clean(self):
"""
Validate the SalesOrderAllocation object:
"""Validate the SalesOrderAllocation object:
- Cannot allocate stock to a line item without a part reference
- The referenced part must match the part associated with the line item
@ -1385,7 +1299,6 @@ class SalesOrderAllocation(models.Model):
- Allocation quantity must be "1" if the StockItem is serialized
- Allocation quantity cannot be zero
"""
super().clean()
errors = {}
@ -1468,13 +1381,11 @@ class SalesOrderAllocation(models.Model):
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"):
- 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,7 +43,8 @@ class AbstractOrderSerializer(serializers.Serializer):
class AbstractExtraLineSerializer(serializers.Serializer):
""" Abstract Serializer for a ExtraLine object """
"""Abstract Serializer for a ExtraLine object"""
def __init__(self, *args, **kwargs):
order_detail = kwargs.pop('order_detail', False)
@ -71,9 +69,7 @@ class AbstractExtraLineSerializer(serializers.Serializer):
class AbstractExtraLineMeta:
"""
Abstract Meta for ExtraLine
"""
"""Abstract Meta for ExtraLine"""
fields = [
'pk',
@ -90,7 +86,7 @@ class AbstractExtraLineMeta:
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrder object """
"""Serializer for a PurchaseOrder object"""
def __init__(self, *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')
)
@ -172,18 +166,13 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
class PurchaseOrderCancelSerializer(serializers.Serializer):
"""
Serializer for cancelling a PurchaseOrder
"""
"""Serializer for cancelling a PurchaseOrder"""
class Meta:
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 {
@ -201,18 +190,13 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
class PurchaseOrderCompleteSerializer(serializers.Serializer):
"""
Serializer for completing a purchase order
"""
"""Serializer for completing a purchase order"""
class Meta:
fields = []
def get_context_data(self):
"""
Custom context information for this serializer
"""
"""Custom context information for this serializer"""
order = self.context['order']
return {
@ -226,7 +210,7 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
class PurchaseOrderIssueSerializer(serializers.Serializer):
""" Serializer for issuing (sending) a purchase order """
"""Serializer for issuing (sending) a purchase order"""
class Meta:
fields = []
@ -241,13 +225,11 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
@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'),
@ -374,7 +356,7 @@ 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)
@ -383,9 +365,7 @@ class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeMod
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:
fields = [
@ -468,10 +448,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
@ -513,9 +490,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)
@ -571,9 +546,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
@ -613,9 +586,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializers for the PurchaseOrderAttachment model
"""
"""Serializers for the PurchaseOrderAttachment model"""
class Meta:
model = order.models.PurchaseOrderAttachment
@ -636,9 +607,7 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
"""
Serializers for the SalesOrder object
"""
"""Serializers for the SalesOrder object"""
def __init__(self, *args, **kwargs):
@ -651,13 +620,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')
)
@ -715,8 +682,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.
"""
@ -783,16 +750,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(
@ -866,9 +831,7 @@ 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)
@ -893,9 +856,7 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
"""
Serializer for completing (shipping) a SalesOrderShipment
"""
"""Serializer for completing (shipping) a SalesOrderShipment"""
class Meta:
model = order.models.SalesOrderShipment
@ -945,9 +906,7 @@ 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:
fields = [
@ -1019,9 +978,7 @@ 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):
@ -1044,8 +1001,7 @@ 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):
@ -1063,9 +1019,7 @@ class SalesOrderCancelSerializer(serializers.Serializer):
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:
fields = [
@ -1084,10 +1038,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 +1070,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 +1086,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']
@ -1226,9 +1173,7 @@ 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:
fields = [
@ -1247,10 +1192,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 +1204,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 +1218,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,7 +1240,7 @@ 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)
@ -1313,9 +1249,7 @@ class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelS
class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializers for the SalesOrderAttachment model
"""
"""Serializers for the SalesOrderAttachment model"""
class Meta:
model = order.models.SalesOrderAttachment

View File

@ -1,6 +1,4 @@
"""
Tests for the Order API
"""
"""Tests for the Order API"""
import io
from datetime import datetime, timedelta
@ -39,10 +37,7 @@ class OrderTest(InvenTreeAPITestCase):
super().setUp()
def filter(self, filters, count):
"""
Test API filters
"""
"""Test API filters"""
response = self.get(
self.LIST_URL,
filters
@ -55,9 +50,7 @@ class OrderTest(InvenTreeAPITestCase):
class PurchaseOrderTest(OrderTest):
"""
Tests for the PurchaseOrder API
"""
"""Tests for the PurchaseOrder API"""
LIST_URL = reverse('api-po-list')
@ -79,10 +72,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)
@ -133,10 +123,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 +210,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 +224,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)
@ -270,7 +251,6 @@ class PurchaseOrderTest(OrderTest):
def test_po_complete(self):
""" Test the PurchaseOrderComplete API endpoint """
po = models.PurchaseOrder.objects.get(pk=3)
url = reverse('api-po-complete', kwargs={'pk': po.pk})
@ -290,7 +270,6 @@ class PurchaseOrderTest(OrderTest):
def test_po_issue(self):
""" Test the PurchaseOrderIssue API endpoint """
po = models.PurchaseOrder.objects.get(pk=2)
url = reverse('api-po-issue', kwargs={'pk': po.pk})
@ -395,9 +374,7 @@ 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):
super().setUp()
@ -415,10 +392,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 +402,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 +418,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 +441,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 +463,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 +485,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 +507,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 +560,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 +637,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 +678,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,9 +734,7 @@ class PurchaseOrderReceiveTest(OrderTest):
class SalesOrderTest(OrderTest):
"""
Tests for the SalesOrder API
"""
"""Tests for the SalesOrder API"""
LIST_URL = reverse('api-so-list')
@ -843,10 +789,7 @@ class SalesOrderTest(OrderTest):
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 +869,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(
@ -980,9 +920,7 @@ class SalesOrderTest(OrderTest):
class SalesOrderLineItemTest(OrderTest):
"""
Tests for the SalesOrderLineItem API
"""
"""Tests for the SalesOrderLineItem API"""
def setUp(self):
@ -1064,7 +1002,6 @@ class SalesOrderDownloadTest(OrderTest):
def test_download_fail(self):
"""Test that downloading without the 'export' option fails"""
url = reverse('api-so-list')
with self.assertRaises(ValueError):
@ -1151,9 +1088,7 @@ 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):
super().setUp()
@ -1188,10 +1123,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 +1176,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)
@ -1279,7 +1207,6 @@ class SalesOrderAllocateTest(OrderTest):
def test_shipment_complete(self):
"""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())

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,5 +1,3 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from django.core.exceptions import ValidationError
@ -15,10 +13,7 @@ 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):
@ -49,9 +44,7 @@ class SalesOrderTest(TestCase):
self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part)
def test_overdue(self):
"""
Tests for overdue functionality
"""
"""Tests for overdue functionality"""
today = datetime.now().date()

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
@ -37,7 +37,7 @@ class OrderListTest(OrderViewTestCase):
class PurchaseOrderTests(OrderViewTestCase):
""" Tests for PurchaseOrder views """
"""Tests for PurchaseOrder views"""
def test_detail_view(self):
""" Retrieve PO detail view """
@ -47,8 +47,7 @@ class PurchaseOrderTests(OrderViewTestCase):
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,5 +1,3 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
import django.core.exceptions as django_exceptions
@ -14,9 +12,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 +26,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/')
@ -43,10 +38,7 @@ class OrderTest(TestCase):
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
def test_overdue(self):
"""
Test overdue status functionality
"""
"""Test overdue status functionality"""
today = datetime.now().date()
order = PurchaseOrder.objects.get(pk=1)
@ -61,8 +53,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 +67,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 +103,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 +135,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,7 +31,7 @@ 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'
@ -61,7 +59,7 @@ class SalesOrderIndex(InvenTreeRoleMixin, ListView):
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')
@ -74,7 +72,7 @@ class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
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,7 +80,7 @@ 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):
REQUIRED_HEADERS = [
@ -126,12 +124,12 @@ 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)
@ -142,11 +140,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 +229,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 +260,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
@ -290,7 +286,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
@ -321,7 +317,7 @@ class PurchaseOrderExport(AjaxView):
class LineItemPricing(PartPricing):
""" View for inspecting part pricing information """
"""View for inspecting part pricing information"""
class EnhancedForm(PartPricing.form_class):
pk = IntegerField(widget=HiddenInput())
@ -365,7 +361,7 @@ 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))

View File

@ -1,6 +1,4 @@
"""
Provides a JSON API for the Part app
"""
"""Provides a JSON API for the Part app"""
import datetime
from decimal import Decimal, InvalidOperation
@ -41,7 +39,7 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
class CategoryList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartCategory objects.
"""API endpoint for accessing a list of PartCategory objects.
- GET: Return a list of PartCategory objects
- POST: Create a new PartCategory object
@ -63,11 +61,10 @@ class CategoryList(generics.ListCreateAPIView):
return ctx
def filter_queryset(self, queryset):
"""
Custom filtering:
"""Custom filtering:
- Allow filtering by "null" parent to retrieve top-level part categories
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -158,9 +155,7 @@ class CategoryList(generics.ListCreateAPIView):
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of a single PartCategory object
"""
"""API endpoint for detail view of a single PartCategory object"""
serializer_class = part_serializers.CategorySerializer
queryset = PartCategory.objects.all()
@ -199,7 +194,7 @@ class CategoryMetadata(generics.RetrieveUpdateAPIView):
class CategoryParameterList(generics.ListAPIView):
""" API endpoint for accessing a list of PartCategoryParameterTemplate objects.
"""API endpoint for accessing a list of PartCategoryParameterTemplate objects.
- GET: Return a list of PartCategoryParameterTemplate objects
"""
@ -208,13 +203,12 @@ class CategoryParameterList(generics.ListAPIView):
serializer_class = part_serializers.CategoryParameterTemplateSerializer
def get_queryset(self):
"""
Custom filtering:
"""Custom filtering:
- Allow filtering by "null" parent to retrieve all categories parameter templates
- Allow filtering by category
- Allow traversing all parent categories
"""
queryset = super().get_queryset()
params = self.request.query_params
@ -241,9 +235,7 @@ class CategoryParameterList(generics.ListAPIView):
class CategoryTree(generics.ListAPIView):
"""
API endpoint for accessing a list of PartCategory objects ready for rendering a tree.
"""
"""API endpoint for accessing a list of PartCategory objects ready for rendering a tree."""
queryset = PartCategory.objects.all()
serializer_class = part_serializers.CategoryTree
@ -258,18 +250,14 @@ class CategoryTree(generics.ListAPIView):
class PartSalePriceDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpoint for PartSellPriceBreak model
"""
"""Detail endpoint for PartSellPriceBreak model"""
queryset = PartSellPriceBreak.objects.all()
serializer_class = part_serializers.PartSalePriceSerializer
class PartSalePriceList(generics.ListCreateAPIView):
"""
API endpoint for list view of PartSalePriceBreak model
"""
"""API endpoint for list view of PartSalePriceBreak model"""
queryset = PartSellPriceBreak.objects.all()
serializer_class = part_serializers.PartSalePriceSerializer
@ -284,18 +272,14 @@ class PartSalePriceList(generics.ListCreateAPIView):
class PartInternalPriceDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpoint for PartInternalPriceBreak model
"""
"""Detail endpoint for PartInternalPriceBreak model"""
queryset = PartInternalPriceBreak.objects.all()
serializer_class = part_serializers.PartInternalPriceSerializer
class PartInternalPriceList(generics.ListCreateAPIView):
"""
API endpoint for list view of PartInternalPriceBreak model
"""
"""API endpoint for list view of PartInternalPriceBreak model"""
queryset = PartInternalPriceBreak.objects.all()
serializer_class = part_serializers.PartInternalPriceSerializer
@ -311,9 +295,7 @@ class PartInternalPriceList(generics.ListCreateAPIView):
class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a PartAttachment (file upload).
"""
"""API endpoint for listing (and creating) a PartAttachment (file upload)."""
queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer
@ -328,38 +310,30 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
"""
Detail endpoint for PartAttachment model
"""
"""Detail endpoint for PartAttachment model"""
queryset = PartAttachment.objects.all()
serializer_class = part_serializers.PartAttachmentSerializer
class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpoint for PartTestTemplate model
"""
"""Detail endpoint for PartTestTemplate model"""
queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer
class PartTestTemplateList(generics.ListCreateAPIView):
"""
API endpoint for listing (and creating) a PartTestTemplate.
"""
"""API endpoint for listing (and creating) a PartTestTemplate."""
queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer
def filter_queryset(self, queryset):
"""
Filter the test list queryset.
"""Filter the test list queryset.
If filtering by 'part', we include results for any parts "above" the specified part.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -390,9 +364,7 @@ class PartTestTemplateList(generics.ListCreateAPIView):
class PartThumbs(generics.ListAPIView):
"""
API endpoint for retrieving information on available Part thumbnails
"""
"""API endpoint for retrieving information on available Part thumbnails"""
queryset = Part.objects.all()
serializer_class = part_serializers.PartThumbSerializer
@ -407,11 +379,10 @@ class PartThumbs(generics.ListAPIView):
return queryset
def list(self, request, *args, **kwargs):
"""
Serialize the available Part images.
"""Serialize the available Part images.
- Images may be used for multiple parts!
"""
queryset = self.filter_queryset(self.get_queryset())
# Return the most popular parts first
@ -436,7 +407,7 @@ class PartThumbs(generics.ListAPIView):
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
""" API endpoint for updating Part thumbnails"""
"""API endpoint for updating Part thumbnails"""
queryset = Part.objects.all()
serializer_class = part_serializers.PartThumbSerializerUpdate
@ -447,8 +418,7 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
class PartScheduling(generics.RetrieveAPIView):
"""
API endpoint for delivering "scheduling" information about a given part via the API.
"""API endpoint for delivering "scheduling" information about a given part via the API.
Returns a chronologically ordered list about future "scheduled" events,
concerning stock levels for the part:
@ -470,13 +440,12 @@ class PartScheduling(generics.RetrieveAPIView):
schedule = []
def add_schedule_entry(date, quantity, title, label, url):
"""
Check if a scheduled entry should be added:
"""Check if a scheduled entry should be added:
- date must be non-null
- date cannot be in the "past"
- quantity must not be zero
"""
if date and date >= today and quantity != 0:
schedule.append({
'date': date,
@ -583,9 +552,7 @@ class PartScheduling(generics.RetrieveAPIView):
class PartMetadata(generics.RetrieveUpdateAPIView):
"""
API endpoint for viewing / updating Part metadata
"""
"""API endpoint for viewing / updating Part metadata"""
def get_serializer(self, *args, **kwargs):
return MetadataSerializer(Part, *args, **kwargs)
@ -594,9 +561,7 @@ class PartMetadata(generics.RetrieveUpdateAPIView):
class PartSerialNumberDetail(generics.RetrieveAPIView):
"""
API endpoint for returning extra serial number information about a particular part
"""
"""API endpoint for returning extra serial number information about a particular part"""
queryset = Part.objects.all()
@ -621,9 +586,7 @@ class PartSerialNumberDetail(generics.RetrieveAPIView):
class PartCopyBOM(generics.CreateAPIView):
"""
API endpoint for duplicating a BOM
"""
"""API endpoint for duplicating a BOM"""
queryset = Part.objects.all()
serializer_class = part_serializers.PartCopyBOMSerializer
@ -641,9 +604,7 @@ class PartCopyBOM(generics.CreateAPIView):
class PartValidateBOM(generics.RetrieveUpdateAPIView):
"""
API endpoint for 'validating' the BOM for a given Part
"""
"""API endpoint for 'validating' the BOM for a given Part"""
class BOMValidateSerializer(serializers.ModelSerializer):
@ -691,7 +652,7 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView):
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single Part object """
"""API endpoint for detail view of a single Part object"""
queryset = Part.objects.all()
serializer_class = part_serializers.PartSerializer
@ -738,12 +699,10 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data=message)
def update(self, request, *args, **kwargs):
"""
Custom update functionality for Part instance.
"""Custom update functionality for Part instance.
- If the 'starred' field is provided, update the 'starred' status against current user
"""
if 'starred' in request.data:
starred = str2bool(request.data.get('starred', False))
@ -755,8 +714,8 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
class PartFilter(rest_filters.FilterSet):
"""
Custom filters for the PartList endpoint.
"""Custom filters for the PartList endpoint.
Uses the django_filters extension framework
"""
@ -791,9 +750,7 @@ class PartFilter(rest_filters.FilterSet):
low_stock = rest_filters.BooleanFilter(label='Low stock', method='filter_low_stock')
def filter_low_stock(self, queryset, name, value):
"""
Filter by "low stock" status
"""
"""Filter by "low stock" status"""
value = str2bool(value)
@ -854,8 +811,7 @@ class PartFilter(rest_filters.FilterSet):
class PartList(APIDownloadMixin, generics.ListCreateAPIView):
"""
API endpoint for accessing a list of Part objects
"""API endpoint for accessing a list of Part objects
- GET: Return list of objects
- POST: Create a new Part object
@ -912,14 +868,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs):
"""
Overide the 'list' method, as the PartCategory objects are
very expensive to serialize!
"""Overide the 'list' method, as the PartCategory objects are very expensive to serialize!
So we will serialize them first, and keep them in memory,
so that they do not have to be serialized multiple times...
So we will serialize them first, and keep them in memory, so that they do not have to be serialized multiple times...
"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
@ -980,12 +932,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
@transaction.atomic
def create(self, request, *args, **kwargs):
"""
We wish to save the user who created this part!
"""We wish to save the user who created this part!
Note: Implementation copied from DRF class CreateModelMixin
"""
# TODO: Unit tests for this function!
serializer = self.get_serializer(data=request.data)
@ -1135,11 +1085,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
return queryset
def filter_queryset(self, queryset):
"""
Perform custom filtering of the queryset.
"""Perform custom filtering of the queryset.
We overide the DRF filter_fields here because
"""
params = self.request.query_params
queryset = super().filter_queryset(queryset)
@ -1391,9 +1340,7 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
class PartRelatedList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of PartRelated objects
"""
"""API endpoint for accessing a list of PartRelated objects"""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
@ -1420,16 +1367,14 @@ class PartRelatedList(generics.ListCreateAPIView):
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for accessing detail view of a PartRelated object
"""
"""API endpoint for accessing detail view of a PartRelated object"""
queryset = PartRelated.objects.all()
serializer_class = part_serializers.PartRelationSerializer
class PartParameterTemplateList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameterTemplate objects.
"""API endpoint for accessing a list of PartParameterTemplate objects.
- GET: Return list of PartParameterTemplate objects
- POST: Create a new PartParameterTemplate object
@ -1453,10 +1398,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
]
def filter_queryset(self, queryset):
"""
Custom filtering for the PartParameterTemplate API
"""
"""Custom filtering for the PartParameterTemplate API"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -1492,7 +1434,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
class PartParameterList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameter objects
"""API endpoint for accessing a list of PartParameter objects
- GET: Return list of PartParameter objects
- POST: Create a new PartParameter object
@ -1512,18 +1454,14 @@ class PartParameterList(generics.ListCreateAPIView):
class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of a single PartParameter object
"""
"""API endpoint for detail view of a single PartParameter object"""
queryset = PartParameter.objects.all()
serializer_class = part_serializers.PartParameterSerializer
class BomFilter(rest_filters.FilterSet):
"""
Custom filters for the BOM list
"""
"""Custom filters for the BOM list"""
# Boolean filters for BOM item
optional = rest_filters.BooleanFilter(label='BOM line is optional')
@ -1564,8 +1502,7 @@ class BomFilter(rest_filters.FilterSet):
class BomList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of BomItem objects.
"""API endpoint for accessing a list of BomItem objects.
- GET: Return list of BomItem objects
- POST: Create a new BomItem object
@ -1715,18 +1652,13 @@ class BomList(generics.ListCreateAPIView):
return queryset
def include_pricing(self):
"""
Determine if pricing information should be included in the response
"""
"""Determine if pricing information should be included in the response"""
pricing_default = InvenTreeSetting.get_setting('PART_SHOW_PRICE_IN_BOM')
return str2bool(self.request.query_params.get('include_pricing', pricing_default))
def annotate_pricing(self, queryset):
"""
Add part pricing information to the queryset
"""
"""Add part pricing information to the queryset"""
# Annotate with purchase prices
queryset = queryset.annotate(
purchase_price_min=Min('sub_part__stock_items__purchase_price'),
@ -1741,8 +1673,7 @@ class BomList(generics.ListCreateAPIView):
).values('pk', 'sub_part', 'purchase_price', 'purchase_price_currency')
def convert_price(price, currency, decimal_places=4):
""" Convert price field, returns Money field """
"""Convert price field, returns Money field"""
price_adjusted = None
# Get default currency from settings
@ -1795,8 +1726,7 @@ class BomList(generics.ListCreateAPIView):
class BomImportUpload(generics.CreateAPIView):
"""
API endpoint for uploading a complete Bill of Materials.
"""API endpoint for uploading a complete Bill of Materials.
It is assumed that the BOM has been extracted from a file using the BomExtract endpoint.
"""
@ -1805,10 +1735,7 @@ class BomImportUpload(generics.CreateAPIView):
serializer_class = part_serializers.BomImportUploadSerializer
def create(self, request, *args, **kwargs):
"""
Custom create function to return the extracted data
"""
"""Custom create function to return the extracted data"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
@ -1820,25 +1747,21 @@ class BomImportUpload(generics.CreateAPIView):
class BomImportExtract(generics.CreateAPIView):
"""
API endpoint for extracting BOM data from a BOM file.
"""
"""API endpoint for extracting BOM data from a BOM file."""
queryset = Part.objects.none()
serializer_class = part_serializers.BomImportExtractSerializer
class BomImportSubmit(generics.CreateAPIView):
"""
API endpoint for submitting BOM data from a BOM file
"""
"""API endpoint for submitting BOM data from a BOM file"""
queryset = BomItem.objects.none()
serializer_class = part_serializers.BomImportSubmitSerializer
class BomDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single BomItem object """
"""API endpoint for detail view of a single BomItem object"""
queryset = BomItem.objects.all()
serializer_class = part_serializers.BomItemSerializer
@ -1854,7 +1777,7 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
class BomItemValidate(generics.UpdateAPIView):
""" API endpoint for validating a BomItem """
"""API endpoint for validating a BomItem"""
# Very simple serializers
class BomItemValidationSerializer(serializers.Serializer):
@ -1883,9 +1806,7 @@ class BomItemValidate(generics.UpdateAPIView):
class BomItemSubstituteList(generics.ListCreateAPIView):
"""
API endpoint for accessing a list of BomItemSubstitute objects
"""
"""API endpoint for accessing a list of BomItemSubstitute objects"""
serializer_class = part_serializers.BomItemSubstituteSerializer
queryset = BomItemSubstitute.objects.all()
@ -1903,9 +1824,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView):
class BomItemSubstituteDetail(generics.RetrieveUpdateDestroyAPIView):
"""
API endpoint for detail view of a single BomItemSubstitute object
"""
"""API endpoint for detail view of a single BomItemSubstitute object"""
queryset = BomItemSubstitute.objects.all()
serializer_class = part_serializers.BomItemSubstituteSerializer

View File

@ -12,21 +12,15 @@ class PartConfig(AppConfig):
name = 'part'
def ready(self):
"""
This function is called whenever the Part app is loaded.
"""
"""This function is called whenever the Part app is loaded."""
if canAppAccessDatabase():
self.update_trackable_status()
def update_trackable_status(self):
"""
Check for any instances where a trackable part is used in the BOM
for a non-trackable part.
"""Check for any instances where a trackable part is used in the BOM for a non-trackable part.
In such a case, force the top-level part to be trackable too.
"""
from .models import BomItem
try:

View File

@ -1,5 +1,4 @@
"""
Functionality for Bill of Material (BOM) management.
"""Functionality for Bill of Material (BOM) management.
Primarily BOM upload tools.
"""
@ -15,14 +14,12 @@ from .models import BomItem
def IsValidBOMFormat(fmt):
""" Test if a file format specifier is in the valid list of BOM file formats """
"""Test if a file format specifier is in the valid list of BOM file formats"""
return fmt.strip().lower() in GetExportFormats()
def MakeBomTemplate(fmt):
""" Generate a Bill of Materials upload template file (for user download) """
"""Generate a Bill of Materials upload template file (for user download)"""
fmt = fmt.strip().lower()
if not IsValidBOMFormat(fmt):
@ -45,13 +42,12 @@ def MakeBomTemplate(fmt):
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False, manufacturer_data=False):
""" Export a BOM (Bill of Materials) for a given part.
"""Export a BOM (Bill of Materials) for a given part.
Args:
fmt: File format (default = 'csv')
cascade: If True, multi-level BOM output is supported. Otherwise, a flat top-level-only BOM is exported.
"""
if not IsValidBOMFormat(fmt):
fmt = 'csv'

View File

@ -1,6 +1,4 @@
"""
Django Forms for interacting with Part objects
"""
"""Django Forms for interacting with Part objects"""
from django import forms
from django.utils.translation import gettext_lazy as _
@ -19,7 +17,7 @@ from .models import (Part, PartCategory, PartCategoryParameterTemplate,
class PartModelChoiceField(forms.ModelChoiceField):
""" Extending string representation of Part instance with available stock """
"""Extending string representation of Part instance with available stock"""
def label_from_instance(self, part):
@ -33,9 +31,7 @@ class PartModelChoiceField(forms.ModelChoiceField):
class PartImageDownloadForm(HelperForm):
"""
Form for downloading an image from a URL
"""
"""Form for downloading an image from a URL"""
url = forms.URLField(
label=_('URL'),
@ -51,10 +47,10 @@ class PartImageDownloadForm(HelperForm):
class BomMatchItemForm(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():
@ -74,13 +70,13 @@ class BomMatchItemForm(MatchItemForm):
class SetPartCategoryForm(forms.Form):
""" Form for setting the category of multiple Part objects """
"""Form for setting the category of multiple Part objects"""
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
class EditPartParameterTemplateForm(HelperForm):
""" Form for editing a PartParameterTemplate object """
"""Form for editing a PartParameterTemplate object"""
class Meta:
model = PartParameterTemplate
@ -91,7 +87,7 @@ class EditPartParameterTemplateForm(HelperForm):
class EditCategoryParameterTemplateForm(HelperForm):
""" Form for editing a PartCategoryParameterTemplate object """
"""Form for editing a PartCategoryParameterTemplate object"""
add_to_same_level_categories = forms.BooleanField(required=False,
initial=False,
@ -113,7 +109,7 @@ class EditCategoryParameterTemplateForm(HelperForm):
class PartPriceForm(forms.Form):
""" Simple form for viewing part pricing information """
"""Simple form for viewing part pricing information"""
quantity = forms.IntegerField(
required=True,
@ -130,9 +126,7 @@ class PartPriceForm(forms.Form):
class EditPartSalePriceBreakForm(HelperForm):
"""
Form for creating / editing a sale price for a part
"""
"""Form for creating / editing a sale price for a part"""
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
@ -146,9 +140,7 @@ class EditPartSalePriceBreakForm(HelperForm):
class EditPartInternalPriceBreakForm(HelperForm):
"""
Form for creating / editing a internal price for a part
"""
"""Form for creating / editing a internal price for a part"""
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))

File diff suppressed because it is too large Load Diff