From 1e90900918347108e782ed906e01d60a38815854 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 May 2022 15:23:57 +0200 Subject: [PATCH] docstring adjustments --- InvenTree/company/models.py | 40 +-- InvenTree/order/__init__.py | 4 +- InvenTree/order/models.py | 203 ++++------- InvenTree/order/serializers.py | 148 +++------ InvenTree/order/test_api.py | 123 ++----- InvenTree/order/test_migrations.py | 46 +-- InvenTree/order/test_sales_order.py | 11 +- InvenTree/order/test_views.py | 7 +- InvenTree/order/tests.py | 26 +- InvenTree/order/urls.py | 3 +- InvenTree/order/views.py | 32 +- InvenTree/part/api.py | 199 ++++------- InvenTree/part/apps.py | 10 +- InvenTree/part/bom.py | 12 +- InvenTree/part/forms.py | 30 +- InvenTree/part/models.py | 499 ++++++++-------------------- 16 files changed, 388 insertions(+), 1005 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 1feac56f67..2391dff749 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -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) diff --git a/InvenTree/order/__init__.py b/InvenTree/order/__init__.py index 896e9facd5..464c173210 100644 --- a/InvenTree/order/__init__.py +++ b/InvenTree/order/__init__.py @@ -1,3 +1 @@ -""" -The Order module is responsible for managing Orders -""" +"""The Order module is responsible for managing Orders""" diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 3871ba38a1..aae8388b70 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -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( diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 0743d95c33..0a6b196002 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -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 diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 6be694bb11..a278fabb13 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -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()) diff --git a/InvenTree/order/test_migrations.py b/InvenTree/order/test_migrations.py index 61299a8e2f..80c88a4006 100644 --- a/InvenTree/order/test_migrations.py +++ b/InvenTree/order/test_migrations.py @@ -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): diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index b357b14f55..1652f18fe1 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -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() diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index aad0fed25d..c905af2cae 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -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) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 1c23be2829..ba95ac8fda 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -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 diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 15e7f5b1bb..278914bd75 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -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 diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 45b7402dc0..caf80c3d0e 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -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)) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index e0fce0e06c..7ab2f1b26a 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -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 diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 9f66be5009..dff059d92a 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -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: diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index fcb86f6204..426855af68 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -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' diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index e1fe938bd2..cf5123f0c0 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -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')) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 0fa6c738aa..9ab99ae7a9 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1,6 +1,4 @@ -""" -Part database model definitions -""" +"""Part database model definitions""" import decimal import hashlib @@ -54,7 +52,7 @@ logger = logging.getLogger("inventree") class PartCategory(MetadataMixin, InvenTreeTree): - """ PartCategory provides hierarchical organization of Part objects. + """PartCategory provides hierarchical organization of Part objects. Attributes: name: Name of this category @@ -64,11 +62,10 @@ class PartCategory(MetadataMixin, InvenTreeTree): """ def delete(self, *args, **kwargs): - """ - Custom model deletion routine, which updates any child categories or parts. + """Custom model deletion routine, which updates any child categories or parts. + This must be handled within a transaction.atomic(), otherwise the tree structure is damaged """ - with transaction.atomic(): parent = self.parent @@ -114,12 +111,11 @@ class PartCategory(MetadataMixin, InvenTreeTree): verbose_name_plural = _("Part Categories") def get_parts(self, cascade=True): - """ Return a queryset for all parts under this category. + """Return a queryset for all parts under this category. args: cascade - If True, also look under subcategories (default = True) """ - if cascade: """ Select any parts which exist in this category or any child categories """ queryset = Part.objects.filter(category__in=self.getUniqueChildren(include_self=True)) @@ -133,10 +129,7 @@ class PartCategory(MetadataMixin, InvenTreeTree): return self.partcount() def partcount(self, cascade=True, active=False): - """ Return the total part count under this category - (including children of child categories) - """ - + """Return the total part count under this category (including children of child categories)""" query = self.get_parts(cascade=cascade) if active: @@ -146,17 +139,15 @@ class PartCategory(MetadataMixin, InvenTreeTree): @property def has_parts(self): - """ True if there are any parts in this category """ + """True if there are any parts in this category""" return self.partcount() > 0 def prefetch_parts_parameters(self, cascade=True): - """ Prefectch parts parameters """ - + """Prefectch parts parameters""" return self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template').all() def get_unique_parameters(self, cascade=True, prefetch=None): - """ Get all unique parameter names for all parts from this category """ - + """Get all unique parameter names for all parts from this category""" unique_parameters_names = [] if prefetch: @@ -173,8 +164,7 @@ class PartCategory(MetadataMixin, InvenTreeTree): return sorted(unique_parameters_names) def get_parts_parameters(self, cascade=True, prefetch=None): - """ Get all parameter names and values for all parts from this category """ - + """Get all parameter names and values for all parts from this category""" category_parameters = [] if prefetch: @@ -203,8 +193,7 @@ class PartCategory(MetadataMixin, InvenTreeTree): @classmethod def get_parent_categories(cls): - """ Return tuple list of parent (root) categories """ - + """Return tuple list of parent (root) categories""" # Get root nodes root_categories = cls.objects.filter(level=0) @@ -215,17 +204,13 @@ class PartCategory(MetadataMixin, InvenTreeTree): return parent_categories def get_parameter_templates(self): - """ Return parameter templates associated to category """ - + """Return parameter templates associated to category""" prefetch = PartCategoryParameterTemplate.objects.prefetch_related('category', 'parameter_template') return prefetch.filter(category=self.id) def get_subscribers(self, include_parents=True): - """ - Return a list of users who subscribe to this PartCategory - """ - + """Return a list of users who subscribe to this PartCategory""" cats = self.get_ancestors(include_self=True) subscribers = set() @@ -245,17 +230,11 @@ class PartCategory(MetadataMixin, InvenTreeTree): return [s for s in subscribers] def is_starred_by(self, user, **kwargs): - """ - Returns True if the specified user subscribes to this category - """ - + """Returns True if the specified user subscribes to this category""" return user in self.get_subscribers(**kwargs) def set_starred(self, user, status): - """ - Set the "subscription" status of this PartCategory against the specified user - """ - + """Set the "subscription" status of this PartCategory against the specified user""" if not user: return @@ -277,7 +256,7 @@ class PartCategory(MetadataMixin, InvenTreeTree): def rename_part_image(instance, filename): - """ Function for renaming a part image file + """Function for renaming a part image file Args: instance: Instance of a Part object @@ -286,7 +265,6 @@ def rename_part_image(instance, filename): Returns: Cleaned filename in format part__img """ - base = 'part_images' fname = os.path.basename(filename) @@ -294,8 +272,7 @@ def rename_part_image(instance, filename): class PartManager(TreeManager): - """ - Defines a custom object manager for the Part model. + """Defines a custom object manager for the Part model. The main purpose of this manager is to reduce the number of database hits, as the Part model has a large number of ForeignKey fields! @@ -313,7 +290,7 @@ class PartManager(TreeManager): @cleanup.ignore class Part(MetadataMixin, MPTTModel): - """ The Part object represents an abstract part, the 'concept' of an actual entity. + """The Part object represents an abstract part, the 'concept' of an actual entity. An actual physical instance of a Part is a StockItem which is treated separately. @@ -368,10 +345,7 @@ class Part(MetadataMixin, MPTTModel): return reverse('api-part-list') def api_instance_filters(self): - """ - Return API query filters for limiting field results against this instance - """ - + """Return API query filters for limiting field results against this instance""" return { 'variant_of': { 'exclude_tree': self.pk, @@ -379,10 +353,7 @@ class Part(MetadataMixin, MPTTModel): } def get_context_data(self, request, **kwargs): - """ - Return some useful context data about this part for template rendering - """ - + """Return some useful context data about this part for template rendering""" context = {} context['disabled'] = not self.active @@ -415,13 +386,12 @@ class Part(MetadataMixin, MPTTModel): return context def save(self, *args, **kwargs): - """ - Overrides the save() function for the Part model. + """Overrides the save() function for the Part model. + If the part image has been updated, then check if the "old" (previous) image is still used by another part. If not, it is considered "orphaned" and will be deleted. """ - # Get category templates settings add_category_templates = kwargs.pop('add_category_templates', False) @@ -479,11 +449,10 @@ class Part(MetadataMixin, MPTTModel): return f"{self.full_name} - {self.description}" def get_parts_in_bom(self, **kwargs): - """ - Return a list of all parts in the BOM for this part. + """Return a list of all parts in the BOM for this part. + Takes into account substitutes, variant parts, and inherited BOM items """ - parts = set() for bom_item in self.get_bom_items(**kwargs): @@ -493,28 +462,23 @@ class Part(MetadataMixin, MPTTModel): return parts def check_if_part_in_bom(self, other_part, **kwargs): - """ - Check if the other_part is in the BOM for *this* part. + """Check if the other_part is in the BOM for *this* part. Note: - Accounts for substitute parts - Accounts for variant BOMs """ - return other_part in self.get_parts_in_bom(**kwargs) def check_add_to_bom(self, parent, raise_error=False, recursive=True): - """ - Check if this Part can be added to the BOM of another part. + """Check if this Part can be added to the BOM of another part. This will fail if: a) The parent part is the same as this one b) The parent part is used in the BOM for *this* part c) The parent part is used in the BOM for any child parts under this one - """ - result = True try: @@ -553,13 +517,11 @@ class Part(MetadataMixin, MPTTModel): return result def checkIfSerialNumberExists(self, sn, exclude_self=False): - """ - Check if a serial number exists for this Part. + """Check if a serial number exists for this Part. Note: Serial numbers must be unique across an entire Part "tree", so here we filter by the entire tree. """ - parts = Part.objects.filter(tree_id=self.tree_id) stock = StockModels.StockItem.objects.filter(part__in=parts, serial=sn) @@ -570,10 +532,7 @@ class Part(MetadataMixin, MPTTModel): return stock.exists() def find_conflicting_serial_numbers(self, serials): - """ - For a provided list of serials, return a list of those which are conflicting. - """ - + """For a provided list of serials, return a list of those which are conflicting.""" conflicts = [] for serial in serials: @@ -583,8 +542,7 @@ class Part(MetadataMixin, MPTTModel): return conflicts def getLatestSerialNumber(self): - """ - Return the "latest" serial number for this Part. + """Return the "latest" serial number for this Part. If *all* the serial numbers are integers, then this will return the highest one. Otherwise, it will simply return the serial number most recently added. @@ -592,7 +550,6 @@ class Part(MetadataMixin, MPTTModel): Note: Serial numbers must be unique across an entire Part "tree", so we filter by the entire tree. """ - parts = Part.objects.filter(tree_id=self.tree_id) stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None) @@ -617,11 +574,10 @@ class Part(MetadataMixin, MPTTModel): return None def getLatestSerialNumberInt(self): - """ - Return the "latest" serial number for this Part as a integer. + """Return the "latest" serial number for this Part as a integer. + If it is not an integer the result is 0 """ - latest = self.getLatestSerialNumber() # No serial number = > 0 @@ -637,11 +593,7 @@ class Part(MetadataMixin, MPTTModel): return 0 def getSerialNumberString(self, quantity=1): - """ - Return a formatted string representing the next available serial numbers, - given a certain quantity of items. - """ - + """Return a formatted string representing the next available serial numbers, given a certain quantity of items.""" latest = self.getLatestSerialNumber() quantity = int(quantity) @@ -674,7 +626,7 @@ class Part(MetadataMixin, MPTTModel): @property def full_name(self): - """ Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in Inventree settings + """Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in Inventree settings As a failsafe option, the following is done @@ -684,7 +636,6 @@ class Part(MetadataMixin, MPTTModel): Elements are joined by the | character """ - full_name_pattern = InvenTreeSetting.get_setting('PART_NAME_FORMAT') try: @@ -726,24 +677,20 @@ class Part(MetadataMixin, MPTTModel): def get_image_url(self): """ Return the URL of the image for this part """ - if self.image: return helpers.getMediaUrl(self.image.url) else: return helpers.getBlankImage() def get_thumbnail_url(self): - """ - Return the URL of the image thumbnail for this part - """ - + """Return the URL of the image thumbnail for this part""" if self.image: return helpers.getMediaUrl(self.image.thumbnail.url) else: return helpers.getBlankThumbnail() def validate_unique(self, exclude=None): - """ Validate that a part is 'unique'. + """Validate that a part is 'unique'. Uniqueness is checked across the following (case insensitive) fields: * Name @@ -752,7 +699,6 @@ class Part(MetadataMixin, MPTTModel): e.g. there can exist multiple parts with the same name, but only if they have a different revision or internal part number. - """ super().validate_unique(exclude) @@ -770,15 +716,13 @@ class Part(MetadataMixin, MPTTModel): }) def clean(self): - """ - Perform cleaning operations for the Part model + """Perform cleaning operations for the Part model Update trackable status: If this part is trackable, and it is used in the BOM for a parent part which is *not* trackable, then we will force the parent part to be trackable. """ - super().clean() # Strip IPN field @@ -875,13 +819,12 @@ class Part(MetadataMixin, MPTTModel): ) def get_default_location(self): - """ Get the default location for a Part (may be None). + """Get the default location for a Part (may be None). If the Part does not specify a default location, look at the Category this part is in. The PartCategory object may also specify a default stock location """ - if self.default_location: return self.default_location elif self.category: @@ -896,13 +839,12 @@ class Part(MetadataMixin, MPTTModel): return None def get_default_supplier(self): - """ Get the default supplier part for this part (may be None). + """Get the default supplier part for this part (may be None). - If the part specifies a default_supplier, return that - If there is only one supplier part available, return that - Else, return None """ - if self.default_supplier: return self.default_supplier @@ -998,8 +940,7 @@ class Part(MetadataMixin, MPTTModel): responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Responsible'), related_name='parts_responible') def format_barcode(self, **kwargs): - """ Return a JSON string for formatting a barcode for this Part object """ - + """Return a JSON string for formatting a barcode for this Part object""" return helpers.MakeBarcode( "part", self.id, @@ -1018,22 +959,17 @@ class Part(MetadataMixin, MPTTModel): @property def available_stock(self): - """ - Return the total available stock. + """Return the total available stock. - This subtracts stock which is already allocated to builds """ - total = self.total_stock total -= self.allocation_count() return max(total, 0) def requiring_build_orders(self): - """ - Return list of outstanding build orders which require this part - """ - + """Return list of outstanding build orders which require this part""" # List parts that this part is required for parts = self.get_used_in().all() @@ -1048,10 +984,7 @@ class Part(MetadataMixin, MPTTModel): return builds def required_build_order_quantity(self): - """ - Return the quantity of this part required for active build orders - """ - + """Return the quantity of this part required for active build orders""" # List active build orders which reference this part builds = self.requiring_build_orders() @@ -1074,10 +1007,7 @@ class Part(MetadataMixin, MPTTModel): return quantity def requiring_sales_orders(self): - """ - Return a list of sales orders which require this part - """ - + """Return a list of sales orders which require this part""" orders = set() # Get a list of line items for open orders which match this part @@ -1092,10 +1022,7 @@ class Part(MetadataMixin, MPTTModel): return orders def required_sales_order_quantity(self): - """ - Return the quantity of this part required for active sales orders - """ - + """Return the quantity of this part required for active sales orders""" # Get a list of line items for open orders which match this part open_lines = OrderModels.SalesOrderLineItem.objects.filter( order__status__in=SalesOrderStatus.OPEN, @@ -1112,16 +1039,12 @@ class Part(MetadataMixin, MPTTModel): return quantity def required_order_quantity(self): - """ - Return total required to fulfil orders - """ - + """Return total required to fulfil orders""" return self.required_build_order_quantity() + self.required_sales_order_quantity() @property def quantity_to_order(self): - """ - Return the quantity needing to be ordered for this part. + """Return the quantity needing to be ordered for this part. Here, an "order" could be one of: - Build Order @@ -1133,9 +1056,7 @@ class Part(MetadataMixin, MPTTModel): Required for orders = self.required_order_quantity() Currently on order = self.on_order Currently building = self.quantity_being_built - """ - # Total requirement required = self.required_order_quantity() @@ -1152,7 +1073,7 @@ class Part(MetadataMixin, MPTTModel): @property def net_stock(self): - """ Return the 'net' stock. It takes into account: + """Return the 'net' stock. It takes into account: - Stock on hand (total_stock) - Stock on order (on_order) @@ -1160,12 +1081,10 @@ class Part(MetadataMixin, MPTTModel): This number (unlike 'available_stock') can be negative. """ - return self.total_stock - self.allocation_count() + self.on_order def get_subscribers(self, include_variants=True, include_categories=True): - """ - Return a list of users who are 'subscribed' to this part. + """Return a list of users who are 'subscribed' to this part. A user may 'subscribe' to this part in the following ways: @@ -1173,9 +1092,7 @@ class Part(MetadataMixin, MPTTModel): b) Subscribing to a template part "above" this part (if it is a variant) c) Subscribing to the part category that this part belongs to d) Subscribing to a parent category of the category in c) - """ - subscribers = set() # Start by looking at direct subscriptions to a Part model @@ -1199,17 +1116,11 @@ class Part(MetadataMixin, MPTTModel): return [s for s in subscribers] def is_starred_by(self, user, **kwargs): - """ - Return True if the specified user subscribes to this part - """ - + """Return True if the specified user subscribes to this part""" return user in self.get_subscribers(**kwargs) def set_starred(self, user, status): - """ - Set the "subscription" status of this Part against the specified user - """ - + """Set the "subscription" status of this Part against the specified user""" if not user: return @@ -1225,20 +1136,15 @@ class Part(MetadataMixin, MPTTModel): PartStar.objects.filter(part=self, user=user).delete() def need_to_restock(self): - """ Return True if this part needs to be restocked - (either by purchasing or building). + """Return True if this part needs to be restocked (either by purchasing or building). - If the allocated_stock exceeds the total_stock, - then we need to restock. + If the allocated_stock exceeds the total_stock, then we need to restock. """ - return (self.total_stock + self.on_order - self.allocation_count) < self.minimum_stock @property def can_build(self): - """ Return the number of units that can be build with available stock - """ - + """Return the number of units that can be build with available stock""" # If this part does NOT have a BOM, result is simply the currently available stock if not self.has_bom: return 0 @@ -1269,27 +1175,23 @@ class Part(MetadataMixin, MPTTModel): @property def active_builds(self): """ Return a list of outstanding builds. + Builds marked as 'complete' or 'cancelled' are ignored """ - return self.builds.filter(status__in=BuildStatus.ACTIVE_CODES) @property def inactive_builds(self): - """ Return a list of inactive builds - """ - + """Return a list of inactive builds""" return self.builds.exclude(status__in=BuildStatus.ACTIVE_CODES) @property def quantity_being_built(self): - """ - Return the current number of parts currently being built. + """Return the current number of parts currently being built. Note: This is the total quantity of Build orders, *not* the number of build outputs. In this fashion, it is the "projected" quantity of builds """ - builds = self.active_builds quantity = 0 @@ -1301,10 +1203,7 @@ class Part(MetadataMixin, MPTTModel): return quantity def build_order_allocations(self, **kwargs): - """ - Return all 'BuildItem' objects which allocate this part to Build objects - """ - + """Return all 'BuildItem' objects which allocate this part to Build objects""" include_variants = kwargs.get('include_variants', True) queryset = BuildModels.BuildItem.objects.all() @@ -1320,10 +1219,7 @@ class Part(MetadataMixin, MPTTModel): return queryset def build_order_allocation_count(self, **kwargs): - """ - Return the total amount of this part allocated to build orders - """ - + """Return the total amount of this part allocated to build orders""" query = self.build_order_allocations(**kwargs).aggregate( total=Coalesce( Sum( @@ -1338,10 +1234,7 @@ class Part(MetadataMixin, MPTTModel): return query['total'] def sales_order_allocations(self, **kwargs): - """ - Return all sales-order-allocation objects which allocate this part to a SalesOrder - """ - + """Return all sales-order-allocation objects which allocate this part to a SalesOrder""" include_variants = kwargs.get('include_variants', True) queryset = OrderModels.SalesOrderAllocation.objects.all() @@ -1375,10 +1268,7 @@ class Part(MetadataMixin, MPTTModel): return queryset def sales_order_allocation_count(self, **kwargs): - """ - Return the total quantity of this part allocated to sales orders - """ - + """Return the total quantity of this part allocated to sales orders""" query = self.sales_order_allocations(**kwargs).aggregate( total=Coalesce( Sum( @@ -1393,11 +1283,8 @@ class Part(MetadataMixin, MPTTModel): return query['total'] def allocation_count(self, **kwargs): + """Return the total quantity of stock allocated for this part, against both build orders and sales orders. """ - Return the total quantity of stock allocated for this part, - against both build orders and sales orders. - """ - return sum( [ self.build_order_allocation_count(**kwargs), @@ -1406,14 +1293,13 @@ class Part(MetadataMixin, MPTTModel): ) def stock_entries(self, include_variants=True, in_stock=None): - """ Return all stock entries for this Part. + """Return all stock entries for this Part. - If this is a template part, include variants underneath this. Note: To return all stock-entries for all part variants under this one, we need to be creative with the filtering. """ - if include_variants: query = StockModels.StockItem.objects.filter(part__in=self.get_descendants(include_self=True)) else: @@ -1427,10 +1313,7 @@ class Part(MetadataMixin, MPTTModel): return query def get_stock_count(self, include_variants=True): - """ - Return the total "in stock" count for this part - """ - + """Return the total "in stock" count for this part""" entries = self.stock_entries(in_stock=True, include_variants=include_variants) query = entries.aggregate(t=Coalesce(Sum('quantity'), Decimal(0))) @@ -1439,17 +1322,15 @@ class Part(MetadataMixin, MPTTModel): @property def total_stock(self): - """ Return the total stock quantity for this part. + """Return the total stock quantity for this part. - Part may be stored in multiple locations - If this part is a "template" (variants exist) then these are counted too """ - return self.get_stock_count(include_variants=True) def get_bom_item_filter(self, include_inherited=True): - """ - Returns a query filter for all BOM items associated with this Part. + """Returns a query filter for all BOM items associated with this Part. There are some considerations: @@ -1463,7 +1344,6 @@ class Part(MetadataMixin, MPTTModel): Because we want to keep our code DRY! """ - bom_filter = Q(part=self) if include_inherited: @@ -1486,25 +1366,21 @@ class Part(MetadataMixin, MPTTModel): return bom_filter def get_bom_items(self, include_inherited=True): - """ - Return a queryset containing all BOM items for this part + """Return a queryset containing all BOM items for this part By default, will include inherited BOM items """ - queryset = BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited)) return queryset.prefetch_related('sub_part') def get_installed_part_options(self, include_inherited=True, include_variants=True): - """ - Return a set of all Parts which can be "installed" into this part, based on the BOM. + """Return a set of all Parts which can be "installed" into this part, based on the BOM. arguments: include_inherited - If set, include BomItem entries defined for parent parts include_variants - If set, include variant parts for BomItems which allow variants """ - parts = set() for bom_item in self.get_bom_items(include_inherited=include_inherited): @@ -1518,8 +1394,7 @@ class Part(MetadataMixin, MPTTModel): return parts def get_used_in_filter(self, include_inherited=True): - """ - Return a query filter for all parts that this part is used in. + """Return a query filter for all parts that this part is used in. There are some considerations: @@ -1529,7 +1404,6 @@ class Part(MetadataMixin, MPTTModel): Note: This function returns a Q object, not an actual queryset. The Q object is used to filter against a list of Part objects """ - # This is pretty expensive - we need to traverse multiple variant lists! # TODO - In the future, could this be improved somehow? @@ -1558,8 +1432,7 @@ class Part(MetadataMixin, MPTTModel): return Q(id__in=part_ids) def get_used_in(self, include_inherited=True): - """ - Return a queryset containing all parts this part is used in. + """Return a queryset containing all parts this part is used in. Includes consideration of inherited BOMs """ @@ -1570,10 +1443,7 @@ class Part(MetadataMixin, MPTTModel): return self.get_bom_items().count() > 0 def get_trackable_parts(self): - """ - Return a queryset of all trackable parts in the BOM for this part - """ - + """Return a queryset of all trackable parts in the BOM for this part""" queryset = self.get_bom_items() queryset = queryset.filter(sub_part__trackable=True) @@ -1581,32 +1451,30 @@ class Part(MetadataMixin, MPTTModel): @property def has_trackable_parts(self): - """ - Return True if any parts linked in the Bill of Materials are trackable. + """Return True if any parts linked in the Bill of Materials are trackable. + This is important when building the part. """ - return self.get_trackable_parts().count() > 0 @property def bom_count(self): - """ Return the number of items contained in the BOM for this part """ + """Return the number of items contained in the BOM for this part""" return self.get_bom_items().count() @property def used_in_count(self): - """ Return the number of part BOMs that this part appears in """ + """Return the number of part BOMs that this part appears in""" return self.get_used_in().count() def get_bom_hash(self): - """ Return a checksum hash for the BOM for this part. + """Return a checksum hash for the BOM for this part. Used to determine if the BOM has changed (and needs to be signed off!) The hash is calculated by hashing each line item in the BOM. returns a string representation of a hash object which can be compared with a stored value """ - result_hash = hashlib.md5(str(self.id).encode()) # List *all* BOM items (including inherited ones!) @@ -1618,19 +1486,16 @@ class Part(MetadataMixin, MPTTModel): return str(result_hash.digest()) def is_bom_valid(self): - """ Check if the BOM is 'valid' - if the calculated checksum matches the stored value - """ - + """Check if the BOM is 'valid' - if the calculated checksum matches the stored value""" return self.get_bom_hash() == self.bom_checksum or not self.has_bom @transaction.atomic def validate_bom(self, user): - """ Validate the BOM (mark the BOM as validated by the given User. + """Validate the BOM (mark the BOM as validated by the given User. - Calculates and stores the hash for the BOM - Saves the current date and the checking user """ - # Validate each line item, ignoring inherited ones bom_items = self.get_bom_items(include_inherited=False) @@ -1645,23 +1510,19 @@ class Part(MetadataMixin, MPTTModel): @transaction.atomic def clear_bom(self): - """ - Clear the BOM items for the part (delete all BOM lines). + """Clear the BOM items for the part (delete all BOM lines). Note: Does *NOT* delete inherited BOM items! """ - self.bom_items.all().delete() def getRequiredParts(self, recursive=False, parts=None): - """ - Return a list of parts required to make this part (i.e. BOM items). + """Return a list of parts required to make this part (i.e. BOM items). Args: recursive: If True iterate down through sub-assemblies parts: Set of parts already found (to prevent recursion issues) """ - if parts is None: parts = set() @@ -1681,13 +1542,11 @@ class Part(MetadataMixin, MPTTModel): return parts def get_allowed_bom_items(self): - """ - Return a list of parts which can be added to a BOM for this part. + """Return a list of parts which can be added to a BOM for this part. - Exclude parts which are not 'component' parts - Exclude parts which this part is in the BOM for """ - # Start with a list of all parts designated as 'sub components' parts = Part.objects.filter(component=True) @@ -1703,17 +1562,17 @@ class Part(MetadataMixin, MPTTModel): @property def supplier_count(self): - """ Return the number of supplier parts available for this part """ + """Return the number of supplier parts available for this part""" return self.supplier_parts.count() @property def has_pricing_info(self, internal=False): - """ Return true if there is pricing information for this part """ + """Return true if there is pricing information for this part""" return self.get_price_range(internal=internal) is not None @property def has_complete_bom_pricing(self): - """ Return true if there is pricing information for each item in the BOM. """ + """Return true if there is pricing information for each item in the BOM.""" use_internal = common.models.get_setting('PART_BOM_USE_INTERNAL_PRICE', False) for item in self.get_bom_items().all().select_related('sub_part'): if not item.sub_part.has_pricing_info(use_internal): @@ -1722,7 +1581,7 @@ class Part(MetadataMixin, MPTTModel): return True def get_price_info(self, quantity=1, buy=True, bom=True, internal=False): - """ Return a simplified pricing string for this part + """Return a simplified pricing string for this part Args: quantity: Number of units to calculate price for @@ -1730,7 +1589,6 @@ class Part(MetadataMixin, MPTTModel): bom: Include BOM pricing (default = True) internal: Include internal pricing (default = False) """ - price_range = self.get_price_range(quantity, buy, bom, internal) if price_range is None: @@ -1773,13 +1631,12 @@ class Part(MetadataMixin, MPTTModel): return (min_price, max_price) def get_bom_price_range(self, quantity=1, internal=False, purchase=False): - """ Return the price range of the BOM for this part. + """Return the price range of the BOM for this part. Adds the minimum price for all components in the BOM. Note: If the BOM contains items without pricing information, these items cannot be included in the BOM! """ - min_price = None max_price = None @@ -1817,8 +1674,7 @@ class Part(MetadataMixin, MPTTModel): return (min_price, max_price) def get_price_range(self, quantity=1, buy=True, bom=True, internal=False, purchase=False): - - """ Return the price range for this part. This price can be either: + """Return the price range for this part. This price can be either: - Supplier price (if purchased from suppliers) - BOM price (if built from other parts) @@ -1828,7 +1684,6 @@ class Part(MetadataMixin, MPTTModel): Returns: Minimum of the supplier, BOM, internal or purchase price. If no pricing available, returns None """ - # only get internal price if set and should be used if internal and self.has_internal_price_breaks: internal_price = self.get_internal_price(quantity) @@ -1867,7 +1722,7 @@ class Part(MetadataMixin, MPTTModel): @property def price_breaks(self): - """ Return the associated price breaks in the correct order """ + """Return the associated price breaks in the correct order""" return self.salepricebreaks.order_by('quantity').all() @property @@ -1875,14 +1730,12 @@ class Part(MetadataMixin, MPTTModel): 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 @@ -1902,7 +1755,7 @@ class Part(MetadataMixin, MPTTModel): @property def internal_price_breaks(self): - """ Return the associated price breaks in the correct order """ + """Return the associated price breaks in the correct order""" return self.internalpricebreaks.order_by('quantity').all() @property @@ -1923,14 +1776,12 @@ class Part(MetadataMixin, MPTTModel): @transaction.atomic def copy_bom_from(self, other, clear=True, **kwargs): - """ - Copy the BOM from another part. + """Copy the BOM from another part. args: other - The part to copy the BOM from clear - Remove existing BOM items first (default=True) """ - # Ignore if the other part is actually this part? if other == self: return @@ -2015,7 +1866,7 @@ class Part(MetadataMixin, MPTTModel): @transaction.atomic def deep_copy(self, other, **kwargs): - """ Duplicates non-field data from another part. + """Duplicates non-field data from another part. Does not alter the normal fields of this part, but can be used to copy other data linked by ForeignKey refernce. @@ -2024,7 +1875,6 @@ class Part(MetadataMixin, MPTTModel): bom: If True, copies BOM data (default = False) parameters: If True, copies Parameters data (default = True) """ - # Copy the part image if kwargs.get('image', True): if other.image: @@ -2050,15 +1900,13 @@ class Part(MetadataMixin, MPTTModel): self.save() def getTestTemplates(self, required=None, include_parent=True): - """ - Return a list of all test templates associated with this Part. + """Return a list of all test templates associated with this Part. These are used for validation of a StockItem. args: required: Set to True or False to filter by "required" status include_parent: Set to True to traverse upwards """ - if include_parent: tests = PartTestTemplate.objects.filter(part__in=self.get_ancestors(include_self=True)) else: @@ -2078,22 +1926,16 @@ class Part(MetadataMixin, MPTTModel): @property def attachment_count(self): - """ - Count the number of attachments for this part. + """Count the number of attachments for this part. + If the part is a variant of a template part, include the number of attachments for the template part. """ - return self.part_attachments.count() @property def part_attachments(self): - """ - Return *all* attachments for this part, - potentially including attachments for template parts - above this one. - """ - + """Return *all* attachments for this part, potentially including attachments for template parts above this one.""" ancestors = self.get_ancestors(include_self=True) attachments = PartAttachment.objects.filter(part__in=ancestors) @@ -2101,8 +1943,7 @@ class Part(MetadataMixin, MPTTModel): return attachments def sales_orders(self): - """ Return a list of sales orders which reference this part """ - + """Return a list of sales orders which reference this part""" orders = [] for line in self.sales_order_line_items.all().prefetch_related('order'): @@ -2112,8 +1953,7 @@ class Part(MetadataMixin, MPTTModel): return orders def purchase_orders(self): - """ Return a list of purchase orders which reference this part """ - + """Return a list of purchase orders which reference this part""" orders = [] for part in self.supplier_parts.all().prefetch_related('purchase_order_line_items'): @@ -2124,19 +1964,16 @@ class Part(MetadataMixin, MPTTModel): return orders def open_purchase_orders(self): - """ Return a list of open purchase orders against this part """ - + """Return a list of open purchase orders against this part""" return [order for order in self.purchase_orders() if order.status in PurchaseOrderStatus.OPEN] def closed_purchase_orders(self): - """ Return a list of closed purchase orders against this part """ - + """Return a list of closed purchase orders against this part""" return [order for order in self.purchase_orders() if order.status not in PurchaseOrderStatus.OPEN] @property def on_order(self): - """ Return the total number of items on order for this part. """ - + """Return the total number of items on order for this part.""" orders = self.supplier_parts.filter(purchase_order_line_items__order__status__in=PurchaseOrderStatus.OPEN).aggregate( quantity=Sum('purchase_order_line_items__quantity'), received=Sum('purchase_order_line_items__received') @@ -2154,20 +1991,17 @@ class Part(MetadataMixin, MPTTModel): return quantity - received def get_parameters(self): - """ Return all parameters for this part, ordered by name """ - + """Return all parameters for this part, ordered by name""" return self.parameters.order_by('template__name') def parameters_map(self): - """ - Return a map (dict) of parameter values assocaited with this Part instance, + """Return a map (dict) of parameter values assocaited with this Part instance, of the form: { "name_1": "value_1", "name_2": "value_2", } """ - params = {} for parameter in self.parameters.all(): @@ -2177,39 +2011,32 @@ class Part(MetadataMixin, MPTTModel): @property def has_variants(self): - """ Check if this Part object has variants underneath it. """ - + """Check if this Part object has variants underneath it.""" return self.get_all_variants().count() > 0 def get_all_variants(self): - """ Return all Part object which exist as a variant under this part. """ - + """Return all Part object which exist as a variant under this part.""" return self.get_descendants(include_self=False) @property def can_convert(self): - """ - Check if this Part can be "converted" to a different variant: + """Check if this Part can be "converted" to a different variant: It can be converted if: a) It has non-virtual variant parts underneath it b) It has non-virtual template parts above it c) It has non-virtual sibling variants - """ - return self.get_conversion_options().count() > 0 def get_conversion_options(self): - """ - Return options for converting this part to a "variant" within the same tree + """Return options for converting this part to a "variant" within the same tree a) Variants underneath this one b) Immediate parent c) Siblings """ - parts = [] # Child parts @@ -2240,11 +2067,10 @@ class Part(MetadataMixin, MPTTModel): return filtered_parts def get_related_parts(self): - """ Return list of tuples for all related parts: + """Return list of tuples for all related parts: - first value is PartRelated object - second value is matching Part object """ - related_parts = [] related_parts_1 = self.related_parts_1.filter(part_1__id=self.pk) @@ -2266,18 +2092,13 @@ class Part(MetadataMixin, MPTTModel): return len(self.get_related_parts()) def is_part_low_on_stock(self): - """ - Returns True if the total stock for this part is less than the minimum stock level - """ - + """Returns True if the total stock for this part is less than the minimum stock level""" return self.get_stock_count() < self.minimum_stock @receiver(post_save, sender=Part, dispatch_uid='part_post_save_log') def after_save_part(sender, instance: Part, created, **kwargs): - """ - Function to be executed after a Part is saved - """ + """Function to be executed after a Part is saved""" from part import tasks as part_tasks if not created and not InvenTree.ready.isImportingData(): @@ -2288,9 +2109,7 @@ def after_save_part(sender, instance: Part, created, **kwargs): class PartAttachment(InvenTreeAttachment): - """ - Model for storing file attachments against a Part object - """ + """Model for storing file attachments against a Part object""" @staticmethod def get_api_url(): @@ -2304,9 +2123,7 @@ class PartAttachment(InvenTreeAttachment): class PartSellPriceBreak(common.models.PriceBreak): - """ - Represents a price break for selling this part - """ + """Represents a price break for selling this part""" @staticmethod def get_api_url(): @@ -2324,9 +2141,7 @@ class PartSellPriceBreak(common.models.PriceBreak): class PartInternalPriceBreak(common.models.PriceBreak): - """ - Represents a price break for internally selling this part - """ + """Represents a price break for internally selling this part""" @staticmethod def get_api_url(): @@ -2343,7 +2158,7 @@ class PartInternalPriceBreak(common.models.PriceBreak): class PartStar(models.Model): - """ A PartStar object creates a subscription relationship between a User and a Part. + """A PartStar object creates a subscription relationship between a User and a Part. It is used to designate a Part as 'subscribed' for a given User. @@ -2364,8 +2179,7 @@ class PartStar(models.Model): class PartCategoryStar(models.Model): - """ - A PartCategoryStar creates a subscription relationship between a User and a PartCategory. + """A PartCategoryStar creates a subscription relationship between a User and a PartCategory. Attributes: category: Link to a PartCategory object @@ -2384,8 +2198,7 @@ class PartCategoryStar(models.Model): class PartTestTemplate(models.Model): - """ - A PartTestTemplate defines a 'template' for a test which is required to be run + """A PartTestTemplate defines a 'template' for a test which is required to be run against a StockItem (an instance of the Part). The test template applies "recursively" to part variants, allowing tests to be @@ -2416,10 +2229,7 @@ class PartTestTemplate(models.Model): super().clean() def validate_unique(self, exclude=None): - """ - Test that this test template is 'unique' within this part tree. - """ - + """Test that this test template is 'unique' within this part tree.""" if not self.part.trackable: raise ValidationError({ 'part': _('Test templates can only be created for trackable parts') @@ -2489,18 +2299,14 @@ class PartTestTemplate(models.Model): def validate_template_name(name): - """ - Prevent illegal characters in "name" field for PartParameterTemplate - """ - + """Prevent illegal characters in "name" field for PartParameterTemplate""" for c in "!@#$%^&*()<>{}[].,?/\\|~`_+-=\'\"": if c in str(name): raise ValidationError(_(f"Illegal character in template name ({c})")) class PartParameterTemplate(models.Model): - """ - A PartParameterTemplate provides a template for key:value pairs for extra + """A PartParameterTemplate provides a template for key:value pairs for extra parameters fields/values to be added to a Part. This allows users to arbitrarily assign data fields to a Part beyond the built-in attributes. @@ -2521,10 +2327,10 @@ class PartParameterTemplate(models.Model): return s def validate_unique(self, exclude=None): - """ Ensure that PartParameterTemplates cannot be created with the same name. + """Ensure that PartParameterTemplates cannot be created with the same name. + This test should be case-insensitive (which the unique caveat does not cover). """ - super().validate_unique(exclude) try: @@ -2550,8 +2356,7 @@ class PartParameterTemplate(models.Model): class PartParameter(models.Model): - """ - A PartParameter is a specific instance of a PartParameterTemplate. It assigns a particular parameter pair to a part. + """A PartParameter is a specific instance of a PartParameterTemplate. It assigns a particular parameter pair to a part. Attributes: part: Reference to a single Part object @@ -2591,8 +2396,7 @@ class PartParameter(models.Model): class PartCategoryParameterTemplate(models.Model): - """ - A PartCategoryParameterTemplate creates a unique relationship between a PartCategory + """A PartCategoryParameterTemplate creates a unique relationship between a PartCategory and a PartParameterTemplate. Multiple PartParameterTemplate instances can be associated to a PartCategory to drive a default list of parameter templates attached to a Part instance upon creation. @@ -2611,8 +2415,7 @@ class PartCategoryParameterTemplate(models.Model): ] def __str__(self): - """ String representation of a PartCategoryParameterTemplate (admin interface) """ - + """String representation of a PartCategoryParameterTemplate (admin interface)""" if self.default_value: return f'{self.category.name} | {self.parameter_template.name} | {self.default_value}' else: @@ -2637,7 +2440,8 @@ class PartCategoryParameterTemplate(models.Model): class BomItem(models.Model, DataImportMixin): - """ A BomItem links a part to its component items. + """A BomItem links a part to its component items. + A part can have a BOM (bill of materials) which defines which parts are required (and in what quantity) to make it. @@ -2692,14 +2496,12 @@ class BomItem(models.Model, DataImportMixin): return reverse('api-bom-list') def get_valid_parts_for_allocation(self, allow_variants=True, allow_substitutes=True): - """ - Return a list of valid parts which can be allocated against this BomItem: + """Return a list of valid parts which can be allocated against this BomItem: - Include the referenced sub_part - Include any directly specvified substitute parts - If allow_variants is True, allow all variants of sub_part """ - # Set of parts we will allow parts = set() @@ -2732,21 +2534,15 @@ class BomItem(models.Model, DataImportMixin): return valid_parts def is_stock_item_valid(self, stock_item): - """ - Check if the provided StockItem object is "valid" for assignment against this BomItem - """ - + """Check if the provided StockItem object is "valid" for assignment against this BomItem""" return stock_item.part in self.get_valid_parts_for_allocation() def get_stock_filter(self): - """ - Return a queryset filter for selecting StockItems which match this BomItem + """Return a queryset filter for selecting StockItems which match this BomItem - Allow stock from all directly specified substitute parts - If allow_variants is True, allow all part variants - """ - return Q(part__in=[part.pk for part in self.get_valid_parts_for_allocation()]) def save(self, *args, **kwargs): @@ -2802,7 +2598,7 @@ class BomItem(models.Model, DataImportMixin): ) def get_item_hash(self): - """ Calculate the checksum hash of this BOM line item: + """Calculate the checksum hash of this BOM line item: The hash is calculated from the following fields: @@ -2812,9 +2608,7 @@ class BomItem(models.Model, DataImportMixin): - Note field - Optional field - Inherited field - """ - # Seed the hash with the ID of this BOM item result_hash = hashlib.md5(str(self.id).encode()) @@ -2830,12 +2624,11 @@ class BomItem(models.Model, DataImportMixin): return str(result_hash.digest()) def validate_hash(self, valid=True): - """ Mark this item as 'valid' (store the checksum hash). + """Mark this item as 'valid' (store the checksum hash). Args: valid: If true, validate the hash, otherwise invalidate it (default = True) """ - if valid: self.checksum = str(self.get_item_hash()) else: @@ -2845,8 +2638,7 @@ class BomItem(models.Model, DataImportMixin): @property def is_line_valid(self): - """ Check if this line item has been validated by the user """ - + """Check if this line item has been validated by the user""" # Ensure an empty checksum returns False if len(self.checksum) == 0: return False @@ -2854,8 +2646,7 @@ class BomItem(models.Model, DataImportMixin): return self.get_item_hash() == self.checksum def clean(self): - """ - Check validity of the BomItem model. + """Check validity of the BomItem model. Performs model checks beyond simple field validation. @@ -2864,7 +2655,6 @@ class BomItem(models.Model, DataImportMixin): - If the "sub_part" is trackable, then the "part" must be trackable too! """ - super().clean() try: @@ -2909,9 +2699,7 @@ class BomItem(models.Model, DataImportMixin): n=decimal2string(self.quantity)) def get_overage_quantity(self, quantity): - """ Calculate overage quantity - """ - + """Calculate overage quantity""" # Most of the time overage string will be empty if len(self.overage) == 0: return 0 @@ -2952,7 +2740,7 @@ class BomItem(models.Model, DataImportMixin): return 0 def get_required_quantity(self, build_quantity): - """ Calculate the required part quantity, based on the supplier build_quantity. + """Calculate the required part quantity, based on the supplier build_quantity. Includes overage estimate in the returned value. Args: @@ -2961,7 +2749,6 @@ class BomItem(models.Model, DataImportMixin): Returns: Quantity required for this build (including overage) """ - # Base quantity requirement base_quantity = self.quantity * build_quantity @@ -2974,8 +2761,7 @@ class BomItem(models.Model, DataImportMixin): @property def price_range(self, internal=False): - """ Return the price-range for this BOM item. """ - + """Return the price-range for this BOM item.""" # get internal price setting use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False) prange = self.sub_part.get_price_range(self.quantity, internal=use_internal and internal) @@ -2996,8 +2782,7 @@ class BomItem(models.Model, DataImportMixin): class BomItemSubstitute(models.Model): - """ - A BomItemSubstitute provides a specification for alternative parts, + """A BomItemSubstitute provides a specification for alternative parts, which can be used in a bill of materials. Attributes: @@ -3018,12 +2803,10 @@ class BomItemSubstitute(models.Model): super().save(*args, **kwargs) def validate_unique(self, exclude=None): - """ - Ensure that this BomItemSubstitute is "unique": + """Ensure that this BomItemSubstitute is "unique": - It cannot point to the same "part" as the "sub_part" of the parent "bom_item" """ - super().validate_unique(exclude=exclude) if self.part == self.bom_item.sub_part: @@ -3056,7 +2839,7 @@ class BomItemSubstitute(models.Model): class PartRelated(models.Model): - """ Store and handle related parts (eg. mating connector, crimps, etc.) """ + """Store and handle related parts (eg. mating connector, crimps, etc.)""" part_1 = models.ForeignKey(Part, related_name='related_parts_1', verbose_name=_('Part 1'), on_delete=models.DO_NOTHING) @@ -3069,8 +2852,7 @@ class PartRelated(models.Model): return f'{self.part_1} <--> {self.part_2}' def validate(self, part_1, part_2): - ''' Validate that the two parts relationship is unique ''' - + """Validate that the two parts relationship is unique""" validate = True parts = Part.objects.all() @@ -3090,8 +2872,7 @@ class PartRelated(models.Model): return validate def clean(self): - ''' Overwrite clean method to check that relation is unique ''' - + """Overwrite clean method to check that relation is unique""" validate = self.validate(self.part_1, self.part_2) if not validate: