2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 19:45:46 +00:00

docstring adjustments

This commit is contained in:
Matthias
2022-05-28 15:23:57 +02:00
parent 60f13ad2e8
commit 1e90900918
16 changed files with 388 additions and 1005 deletions

View File

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

View File

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

View File

@ -1,8 +1,4 @@
""" """Order model definitions"""
Order model definitions
"""
# -*- coding: utf-8 -*-
import logging import logging
import os import os
@ -47,10 +43,7 @@ logger = logging.getLogger('inventree')
def get_next_po_number(): def get_next_po_number():
""" """Returns the next available PurchaseOrder reference number"""
Returns the next available PurchaseOrder reference number
"""
if PurchaseOrder.objects.count() == 0: if PurchaseOrder.objects.count() == 0:
return '0001' return '0001'
@ -76,10 +69,7 @@ def get_next_po_number():
def get_next_so_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: if SalesOrder.objects.count() == 0:
return '0001' return '0001'
@ -105,7 +95,7 @@ def get_next_so_number():
class Order(MetadataMixin, ReferenceIndexingMixin): class Order(MetadataMixin, ReferenceIndexingMixin):
""" Abstract model for an order. """Abstract model for an order.
Instances of this class: Instances of this class:
@ -159,15 +149,13 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes')) notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
def get_total_price(self, target_currency=currency_code_default()): 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 not specified, the default system currency is used.
If currency conversion fails (e.g. there are no valid conversion rates), If currency conversion fails (e.g. there are no valid conversion rates),
then we simply return zero, rather than attempting some other calculation. then we simply return zero, rather than attempting some other calculation.
""" """
total = Money(0, target_currency) total = Money(0, target_currency)
# gather name reference # gather name reference
@ -230,7 +218,7 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
class PurchaseOrder(Order): class PurchaseOrder(Order):
""" A PurchaseOrder represents goods shipped inwards from an external supplier. """A PurchaseOrder represents goods shipped inwards from an external supplier.
Attributes: Attributes:
supplier: Reference to the company supplying the goods in the order supplier: Reference to the company supplying the goods in the order
@ -247,8 +235,7 @@ class PurchaseOrder(Order):
@staticmethod @staticmethod
def filterByDate(queryset, min_date, max_date): 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 - Specified as min_date, max_date
- Both must be specified for filter to be applied - 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 - A "pending" order where the target date lies within the date range
- TODO: An "overdue" order where the target date is in the past - TODO: An "overdue" order where the target date is in the past
""" """
date_fmt = '%Y-%m-%d' # ISO format date string date_fmt = '%Y-%m-%d' # ISO format date string
# Ensure that both dates are valid # Ensure that both dates are valid
@ -344,7 +330,7 @@ class PurchaseOrder(Order):
@transaction.atomic @transaction.atomic
def add_line_item(self, supplier_part, quantity, group=True, reference='', purchase_price=None): 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: This function will check that:
* The supplier part matches the supplier specified for this purchase order * 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 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) group - If True, this new quantity will be added to an existing line item for the same supplier_part (if it exists)
""" """
try: try:
quantity = int(quantity) quantity = int(quantity)
if quantity <= 0: if quantity <= 0:
@ -396,8 +381,7 @@ class PurchaseOrder(Order):
@transaction.atomic @transaction.atomic
def place_order(self): 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: if self.status == PurchaseOrderStatus.PENDING:
self.status = PurchaseOrderStatus.PLACED self.status = PurchaseOrderStatus.PLACED
self.issue_date = datetime.now().date() self.issue_date = datetime.now().date()
@ -407,8 +391,7 @@ class PurchaseOrder(Order):
@transaction.atomic @transaction.atomic
def complete_order(self): 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: if self.status == PurchaseOrderStatus.PLACED:
self.status = PurchaseOrderStatus.COMPLETE self.status = PurchaseOrderStatus.COMPLETE
self.complete_date = datetime.now().date() self.complete_date = datetime.now().date()
@ -418,22 +401,17 @@ class PurchaseOrder(Order):
@property @property
def is_overdue(self): 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. Makes use of the OVERDUE_FILTER to avoid code duplication.
""" """
query = PurchaseOrder.objects.filter(pk=self.pk) query = PurchaseOrder.objects.filter(pk=self.pk)
query = query.filter(PurchaseOrder.OVERDUE_FILTER) query = query.filter(PurchaseOrder.OVERDUE_FILTER)
return query.exists() return query.exists()
def can_cancel(self): 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 [ return self.status in [
PurchaseOrderStatus.PLACED, PurchaseOrderStatus.PLACED,
PurchaseOrderStatus.PENDING PurchaseOrderStatus.PENDING
@ -441,8 +419,7 @@ class PurchaseOrder(Order):
@transaction.atomic @transaction.atomic
def cancel_order(self): def cancel_order(self):
""" Marks the PurchaseOrder as CANCELLED. """ """Marks the PurchaseOrder as CANCELLED."""
if self.can_cancel(): if self.can_cancel():
self.status = PurchaseOrderStatus.CANCELLED self.status = PurchaseOrderStatus.CANCELLED
self.save() self.save()
@ -450,16 +427,14 @@ class PurchaseOrder(Order):
trigger_event('purchaseorder.cancelled', id=self.pk) trigger_event('purchaseorder.cancelled', id=self.pk)
def pending_line_items(self): 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. Any line item where 'received' < 'quantity' will be returned.
""" """
return self.lines.filter(quantity__gt=F('received')) return self.lines.filter(quantity__gt=F('received'))
def completed_line_items(self): 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')) return self.lines.filter(quantity__lte=F('received'))
@property @property
@ -477,16 +452,12 @@ class PurchaseOrder(Order):
@property @property
def is_complete(self): 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 return self.lines.count() > 0 and self.pending_line_items().count() == 0
@transaction.atomic @transaction.atomic
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs): 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 # Extract optional batch code for the new stock item
batch_code = kwargs.get('batch_code', '') batch_code = kwargs.get('batch_code', '')
@ -573,8 +544,7 @@ class PurchaseOrder(Order):
class SalesOrder(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: Attributes:
customer: Reference to the company receiving the goods in the order customer: Reference to the company receiving the goods in the order
@ -590,8 +560,7 @@ class SalesOrder(Order):
@staticmethod @staticmethod
def filterByDate(queryset, min_date, max_date): 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 - Specified as min_date, max_date
- Both must be specified for filter to be applied - 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 - A "pending" order where the target date lies within the date range
- TODO: An "overdue" order where the target date is in the past - TODO: An "overdue" order where the target date is in the past
""" """
date_fmt = '%Y-%m-%d' # ISO format date string date_fmt = '%Y-%m-%d' # ISO format date string
# Ensure that both dates are valid # Ensure that both dates are valid
@ -682,12 +650,10 @@ class SalesOrder(Order):
@property @property
def is_overdue(self): 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. Makes use of the OVERDUE_FILTER to avoid code duplication.
""" """
query = SalesOrder.objects.filter(pk=self.pk) query = SalesOrder.objects.filter(pk=self.pk)
query = query.filter(SalesOrder.OVERDUE_FILTER) query = query.filter(SalesOrder.OVERDUE_FILTER)
@ -699,17 +665,13 @@ class SalesOrder(Order):
@property @property
def stock_allocations(self): 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( return SalesOrderAllocation.objects.filter(
line__in=[line.pk for line in self.lines.all()] line__in=[line.pk for line in self.lines.all()]
) )
def is_fully_allocated(self): 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(): for line in self.lines.all():
if not line.is_fully_allocated(): if not line.is_fully_allocated():
return False return False
@ -717,8 +679,7 @@ class SalesOrder(Order):
return True return True
def is_over_allocated(self): 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(): for line in self.lines.all():
if line.is_over_allocated(): if line.is_over_allocated():
return True return True
@ -726,19 +687,14 @@ class SalesOrder(Order):
return False return False
def is_completed(self): 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()]) return self.lines.count() > 0 and all([line.is_completed() for line in self.lines.all()])
def can_complete(self, raise_error=False): 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. Throws a ValidationError if cannot be completed.
""" """
try: try:
# Order without line items cannot be completed # Order without line items cannot be completed
@ -765,10 +721,7 @@ class SalesOrder(Order):
return True return True
def complete_order(self, user): def complete_order(self, user):
""" """Mark this order as "complete"""
Mark this order as "complete"
"""
if not self.can_complete(): if not self.can_complete():
return False return False
@ -783,10 +736,7 @@ class SalesOrder(Order):
return True return True
def can_cancel(self): 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: if self.status != SalesOrderStatus.PENDING:
return False return False
@ -794,13 +744,11 @@ class SalesOrder(Order):
@transaction.atomic @transaction.atomic
def cancel_order(self): 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' - Mark the order as 'cancelled'
- Delete any StockItems which have been allocated - Delete any StockItems which have been allocated
""" """
if not self.can_cancel(): if not self.can_cancel():
return False return False
@ -820,15 +768,11 @@ class SalesOrder(Order):
return self.lines.count() return self.lines.count()
def completed_line_items(self): 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')) return self.lines.filter(shipped__gte=F('quantity'))
def pending_line_items(self): 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')) return self.lines.filter(shipped__lt=F('quantity'))
@property @property
@ -840,16 +784,11 @@ class SalesOrder(Order):
return self.pending_line_items().count() return self.pending_line_items().count()
def completed_shipments(self): 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) return self.shipments.exclude(shipment_date=None)
def pending_shipments(self): 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) return self.shipments.filter(shipment_date=None)
@property @property
@ -867,9 +806,7 @@ class SalesOrder(Order):
@receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log') @receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log')
def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs): 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'): if created and getSetting('SALESORDER_DEFAULT_SHIPMENT'):
# A new SalesOrder has just been created # 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): class PurchaseOrderAttachment(InvenTreeAttachment):
""" """Model for storing file attachments against a PurchaseOrder object"""
Model for storing file attachments against a PurchaseOrder object
"""
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
@ -896,9 +831,7 @@ class PurchaseOrderAttachment(InvenTreeAttachment):
class SalesOrderAttachment(InvenTreeAttachment): class SalesOrderAttachment(InvenTreeAttachment):
""" """Model for storing file attachments against a SalesOrder object"""
Model for storing file attachments against a SalesOrder object
"""
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
@ -911,7 +844,7 @@ class SalesOrderAttachment(InvenTreeAttachment):
class OrderLineItem(models.Model): class OrderLineItem(models.Model):
""" Abstract model for an order line item """Abstract model for an order line item
Attributes: Attributes:
quantity: Number of items quantity: Number of items
@ -951,8 +884,8 @@ class OrderLineItem(models.Model):
class OrderExtraLine(OrderLineItem): class OrderExtraLine(OrderLineItem):
""" """Abstract Model for a single ExtraLine in a Order
Abstract Model for a single ExtraLine in a Order
Attributes: Attributes:
price: The unit sale price for this OrderLineItem price: The unit sale price for this OrderLineItem
""" """
@ -984,7 +917,7 @@ class OrderExtraLine(OrderLineItem):
class PurchaseOrderLineItem(OrderLineItem): class PurchaseOrderLineItem(OrderLineItem):
""" Model for a purchase order line item. """Model for a purchase order line item.
Attributes: Attributes:
order: Reference to a PurchaseOrder object order: Reference to a PurchaseOrder object
@ -1024,8 +957,7 @@ class PurchaseOrderLineItem(OrderLineItem):
) )
def get_base_part(self): 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! Note: Returns None if the SupplierPart is not set!
""" """
@ -1067,14 +999,12 @@ class PurchaseOrderLineItem(OrderLineItem):
) )
def get_destination(self): 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 NOTE: If a line item gets split when recieved, only an arbitrary
stock items location will be reported as the location for the stock items location will be reported as the location for the
entire line. entire line.
""" """
for stock in stock_models.StockItem.objects.filter(supplier_part=self.part, purchase_order=self.order): for stock in stock_models.StockItem.objects.filter(supplier_part=self.part, purchase_order=self.order):
if stock.location: if stock.location:
return stock.location return stock.location
@ -1084,14 +1014,13 @@ class PurchaseOrderLineItem(OrderLineItem):
return self.part.part.default_location return self.part.part.default_location
def remaining(self): 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 r = self.quantity - self.received
return max(r, 0) return max(r, 0)
class PurchaseOrderExtraLine(OrderExtraLine): class PurchaseOrderExtraLine(OrderExtraLine):
""" """Model for a single ExtraLine in a PurchaseOrder
Model for a single ExtraLine in a PurchaseOrder
Attributes: Attributes:
order: Link to the PurchaseOrder that this line belongs to order: Link to the PurchaseOrder that this line belongs to
title: title of line title: title of line
@ -1105,8 +1034,7 @@ class PurchaseOrderExtraLine(OrderExtraLine):
class SalesOrderLineItem(OrderLineItem): class SalesOrderLineItem(OrderLineItem):
""" """Model for a single LineItem in a SalesOrder
Model for a single LineItem in a SalesOrder
Attributes: Attributes:
order: Link to the SalesOrder that this line item belongs to order: Link to the SalesOrder that this line item belongs to
@ -1150,47 +1078,38 @@ class SalesOrderLineItem(OrderLineItem):
] ]
def fulfilled_quantity(self): 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))) query = self.order.stock_items.filter(part=self.part).aggregate(fulfilled=Coalesce(Sum('quantity'), Decimal(0)))
return query['fulfilled'] return query['fulfilled']
def allocated_quantity(self): 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 This is a summation of the quantity of each attached StockItem
""" """
query = self.allocations.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0))) query = self.allocations.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0)))
return query['allocated'] return query['allocated']
def is_fully_allocated(self): 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: if self.order.status == SalesOrderStatus.SHIPPED:
return self.fulfilled_quantity() >= self.quantity return self.fulfilled_quantity() >= self.quantity
return self.allocated_quantity() >= self.quantity return self.allocated_quantity() >= self.quantity
def is_over_allocated(self): 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 return self.allocated_quantity() > self.quantity
def is_completed(self): 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 return self.shipped >= self.quantity
class SalesOrderShipment(models.Model): 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 - Points to a single SalesOrder object
- Multiple SalesOrderAllocation objects point to a particular SalesOrderShipment - Multiple SalesOrderAllocation objects point to a particular SalesOrderShipment
@ -1297,14 +1216,12 @@ class SalesOrderShipment(models.Model):
@transaction.atomic @transaction.atomic
def complete_shipment(self, user, **kwargs): def complete_shipment(self, user, **kwargs):
""" """Complete this particular shipment:
Complete this particular shipment:
1. Update any stock items associated with this shipment 1. Update any stock items associated with this shipment
2. Update the "shipped" quantity of all associated line items 2. Update the "shipped" quantity of all associated line items
3. Set the "shipment_date" to now 3. Set the "shipment_date" to now
""" """
# Check if the shipment can be completed (throw error if not) # Check if the shipment can be completed (throw error if not)
self.check_can_complete() self.check_can_complete()
@ -1343,8 +1260,8 @@ class SalesOrderShipment(models.Model):
class SalesOrderExtraLine(OrderExtraLine): class SalesOrderExtraLine(OrderExtraLine):
""" """Model for a single ExtraLine in a SalesOrder
Model for a single ExtraLine in a SalesOrder
Attributes: Attributes:
order: Link to the SalesOrder that this line belongs to order: Link to the SalesOrder that this line belongs to
title: title of line title: title of line
@ -1358,8 +1275,7 @@ class SalesOrderExtraLine(OrderExtraLine):
class SalesOrderAllocation(models.Model): 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, Items that are "allocated" to a SalesOrder are not yet "attached" to the order,
but they will be once the order is fulfilled. but they will be once the order is fulfilled.
@ -1368,7 +1284,6 @@ class SalesOrderAllocation(models.Model):
shipment: SalesOrderShipment reference shipment: SalesOrderShipment reference
item: StockItem reference item: StockItem reference
quantity: Quantity to take from the StockItem quantity: Quantity to take from the StockItem
""" """
@staticmethod @staticmethod
@ -1376,8 +1291,7 @@ class SalesOrderAllocation(models.Model):
return reverse('api-so-allocation-list') return reverse('api-so-allocation-list')
def clean(self): def clean(self):
""" """Validate the SalesOrderAllocation object:
Validate the SalesOrderAllocation object:
- Cannot allocate stock to a line item without a part reference - Cannot allocate stock to a line item without a part reference
- The referenced part must match the part associated with the line item - 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 must be "1" if the StockItem is serialized
- Allocation quantity cannot be zero - Allocation quantity cannot be zero
""" """
super().clean() super().clean()
errors = {} errors = {}
@ -1468,13 +1381,11 @@ class SalesOrderAllocation(models.Model):
return self.item.purchase_order return self.item.purchase_order
def complete_allocation(self, user): 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) - 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) - Mark the StockItem as belonging to the Customer (this will remove it from stock)
""" """
order = self.line.order order = self.line.order
item = self.item.allocateToCustomer( item = self.item.allocateToCustomer(

View File

@ -1,6 +1,4 @@
""" """JSON serializers for the Order API"""
JSON serializers for the Order API
"""
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
@ -33,9 +31,8 @@ from users.serializers import OwnerSerializer
class AbstractOrderSerializer(serializers.Serializer): class AbstractOrderSerializer(serializers.Serializer):
""" """Abstract field definitions for OrderSerializers"""
Abstract field definitions for OrderSerializers
"""
total_price = InvenTreeMoneySerializer( total_price = InvenTreeMoneySerializer(
source='get_total_price', source='get_total_price',
allow_null=True, allow_null=True,
@ -46,7 +43,8 @@ class AbstractOrderSerializer(serializers.Serializer):
class AbstractExtraLineSerializer(serializers.Serializer): class AbstractExtraLineSerializer(serializers.Serializer):
""" Abstract Serializer for a ExtraLine object """ """Abstract Serializer for a ExtraLine object"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
order_detail = kwargs.pop('order_detail', False) order_detail = kwargs.pop('order_detail', False)
@ -71,9 +69,7 @@ class AbstractExtraLineSerializer(serializers.Serializer):
class AbstractExtraLineMeta: class AbstractExtraLineMeta:
""" """Abstract Meta for ExtraLine"""
Abstract Meta for ExtraLine
"""
fields = [ fields = [
'pk', 'pk',
@ -90,7 +86,7 @@ class AbstractExtraLineMeta:
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrder object """ """Serializer for a PurchaseOrder object"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -103,13 +99,11 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
""" """Add extra information to the queryset
Add extra information to the queryset
- Number of lines in the PurchaseOrder - Number of lines in the PurchaseOrder
- Overdue status of the PurchaseOrder - Overdue status of the PurchaseOrder
""" """
queryset = queryset.annotate( queryset = queryset.annotate(
line_items=SubqueryCount('lines') line_items=SubqueryCount('lines')
) )
@ -172,18 +166,13 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
class PurchaseOrderCancelSerializer(serializers.Serializer): class PurchaseOrderCancelSerializer(serializers.Serializer):
""" """Serializer for cancelling a PurchaseOrder"""
Serializer for cancelling a PurchaseOrder
"""
class Meta: class Meta:
fields = [], fields = [],
def get_context_data(self): def get_context_data(self):
""" """Return custom context information about the order"""
Return custom context information about the order
"""
self.order = self.context['order'] self.order = self.context['order']
return { return {
@ -201,18 +190,13 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
class PurchaseOrderCompleteSerializer(serializers.Serializer): class PurchaseOrderCompleteSerializer(serializers.Serializer):
""" """Serializer for completing a purchase order"""
Serializer for completing a purchase order
"""
class Meta: class Meta:
fields = [] fields = []
def get_context_data(self): def get_context_data(self):
""" """Custom context information for this serializer"""
Custom context information for this serializer
"""
order = self.context['order'] order = self.context['order']
return { return {
@ -226,7 +210,7 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
class PurchaseOrderIssueSerializer(serializers.Serializer): class PurchaseOrderIssueSerializer(serializers.Serializer):
""" Serializer for issuing (sending) a purchase order """ """Serializer for issuing (sending) a purchase order"""
class Meta: class Meta:
fields = [] fields = []
@ -241,13 +225,11 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
""" """Add some extra annotations to this queryset:
Add some extra annotations to this queryset:
- Total price = purchase_price * quantity - Total price = purchase_price * quantity
- "Overdue" status (boolean field) - "Overdue" status (boolean field)
""" """
queryset = queryset.annotate( queryset = queryset.annotate(
total_price=ExpressionWrapper( total_price=ExpressionWrapper(
F('purchase_price') * F('quantity'), F('purchase_price') * F('quantity'),
@ -374,7 +356,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, 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) order_detail = PurchaseOrderSerializer(source='order', many=False, read_only=True)
@ -383,9 +365,7 @@ class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeMod
class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer): 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: class Meta:
fields = [ fields = [
@ -468,10 +448,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
) )
def validate_barcode(self, barcode): 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 # Ignore empty barcode values
if not barcode or barcode.strip() == '': if not barcode or barcode.strip() == '':
return None return None
@ -513,9 +490,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
class PurchaseOrderReceiveSerializer(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) items = PurchaseOrderLineItemReceiveSerializer(many=True)
@ -571,9 +546,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
return data return data
def save(self): 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 data = self.validated_data
@ -613,9 +586,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer): class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """Serializers for the PurchaseOrderAttachment model"""
Serializers for the PurchaseOrderAttachment model
"""
class Meta: class Meta:
model = order.models.PurchaseOrderAttachment model = order.models.PurchaseOrderAttachment
@ -636,9 +607,7 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" """Serializers for the SalesOrder object"""
Serializers for the SalesOrder object
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -651,13 +620,11 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
""" """Add extra information to the queryset
Add extra information to the queryset
- Number of line items in the SalesOrder - Number of line items in the SalesOrder
- Overdue status of the SalesOrder - Overdue status of the SalesOrder
""" """
queryset = queryset.annotate( queryset = queryset.annotate(
line_items=SubqueryCount('lines') line_items=SubqueryCount('lines')
) )
@ -715,8 +682,8 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
class SalesOrderAllocationSerializer(InvenTreeModelSerializer): class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
""" """Serializer for the SalesOrderAllocation model.
Serializer for the SalesOrderAllocation model.
This includes some fields from the related model objects. This includes some fields from the related model objects.
""" """
@ -783,16 +750,14 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
class SalesOrderLineItemSerializer(InvenTreeModelSerializer): class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
""" Serializer for a SalesOrderLineItem object """ """Serializer for a SalesOrderLineItem object"""
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
""" """Add some extra annotations to this queryset:
Add some extra annotations to this queryset:
- "Overdue" status (boolean field) - "Overdue" status (boolean field)
""" """
queryset = queryset.annotate( queryset = queryset.annotate(
overdue=Case( overdue=Case(
When( When(
@ -866,9 +831,7 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
class SalesOrderShipmentSerializer(InvenTreeModelSerializer): class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
""" """Serializer for the SalesOrderShipment class"""
Serializer for the SalesOrderShipment class
"""
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
@ -893,9 +856,7 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer): class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
""" """Serializer for completing (shipping) a SalesOrderShipment"""
Serializer for completing (shipping) a SalesOrderShipment
"""
class Meta: class Meta:
model = order.models.SalesOrderShipment model = order.models.SalesOrderShipment
@ -945,9 +906,7 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer): 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: class Meta:
fields = [ fields = [
@ -1019,9 +978,7 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
class SalesOrderCompleteSerializer(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): def validate(self, data):
@ -1044,8 +1001,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
class SalesOrderCancelSerializer(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): def get_context_data(self):
@ -1063,9 +1019,7 @@ class SalesOrderCancelSerializer(serializers.Serializer):
class SalesOrderSerialAllocationSerializer(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: class Meta:
fields = [ fields = [
@ -1084,10 +1038,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
) )
def validate_line_item(self, line_item): 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'] order = self.context['order']
# Ensure that the line item points to the correct order # Ensure that the line item points to the correct order
@ -1119,13 +1070,11 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
) )
def validate_shipment(self, shipment): def validate_shipment(self, shipment):
""" """Validate the shipment:
Validate the shipment:
- Must point to the same order - Must point to the same order
- Must not be shipped - Must not be shipped
""" """
order = self.context['order'] order = self.context['order']
if shipment.shipment_date is not None: if shipment.shipment_date is not None:
@ -1137,14 +1086,12 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
return shipment return shipment
def validate(self, data): def validate(self, data):
""" """Validation for the serializer:
Validation for the serializer:
- Ensure the serial_numbers and quantity fields match - Ensure the serial_numbers and quantity fields match
- Check that all serial numbers exist - Check that all serial numbers exist
- Check that the serial numbers are not yet allocated - Check that the serial numbers are not yet allocated
""" """
data = super().validate(data) data = super().validate(data)
line_item = data['line_item'] line_item = data['line_item']
@ -1226,9 +1173,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
class SalesOrderShipmentAllocationSerializer(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: class Meta:
fields = [ fields = [
@ -1247,10 +1192,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
) )
def validate_shipment(self, shipment): def validate_shipment(self, shipment):
""" """Run validation against the provided shipment instance"""
Run validation against the provided shipment instance
"""
order = self.context['order'] order = self.context['order']
if shipment.shipment_date is not None: if shipment.shipment_date is not None:
@ -1262,10 +1204,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
return shipment return shipment
def validate(self, data): def validate(self, data):
""" """Serializer validation"""
Serializer validation
"""
data = super().validate(data) data = super().validate(data)
# Extract SalesOrder from serializer context # Extract SalesOrder from serializer context
@ -1279,10 +1218,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
return data return data
def save(self): def save(self):
""" """Perform the allocation of items against this order"""
Perform the allocation of items against this order
"""
data = self.validated_data data = self.validated_data
items = data['items'] items = data['items']
@ -1304,7 +1240,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer): class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
""" Serializer for a SalesOrderExtraLine object """ """Serializer for a SalesOrderExtraLine object"""
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
@ -1313,9 +1249,7 @@ class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelS
class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer): class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
""" """Serializers for the SalesOrderAttachment model"""
Serializers for the SalesOrderAttachment model
"""
class Meta: class Meta:
model = order.models.SalesOrderAttachment model = order.models.SalesOrderAttachment

View File

@ -1,6 +1,4 @@
""" """Tests for the Order API"""
Tests for the Order API
"""
import io import io
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -39,10 +37,7 @@ class OrderTest(InvenTreeAPITestCase):
super().setUp() super().setUp()
def filter(self, filters, count): def filter(self, filters, count):
""" """Test API filters"""
Test API filters
"""
response = self.get( response = self.get(
self.LIST_URL, self.LIST_URL,
filters filters
@ -55,9 +50,7 @@ class OrderTest(InvenTreeAPITestCase):
class PurchaseOrderTest(OrderTest): class PurchaseOrderTest(OrderTest):
""" """Tests for the PurchaseOrder API"""
Tests for the PurchaseOrder API
"""
LIST_URL = reverse('api-po-list') LIST_URL = reverse('api-po-list')
@ -79,10 +72,7 @@ class PurchaseOrderTest(OrderTest):
self.filter({'status': 40}, 1) self.filter({'status': 40}, 1)
def test_overdue(self): def test_overdue(self):
""" """Test "overdue" status"""
Test "overdue" status
"""
self.filter({'overdue': True}, 0) self.filter({'overdue': True}, 0)
self.filter({'overdue': False}, 7) self.filter({'overdue': False}, 7)
@ -133,10 +123,7 @@ class PurchaseOrderTest(OrderTest):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_po_operations(self): 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() n = models.PurchaseOrder.objects.count()
url = reverse('api-po-list') url = reverse('api-po-list')
@ -223,10 +210,7 @@ class PurchaseOrderTest(OrderTest):
response = self.get(url, expected_code=404) response = self.get(url, expected_code=404)
def test_po_create(self): 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.assignRole('purchase_order.add')
self.post( self.post(
@ -240,10 +224,7 @@ class PurchaseOrderTest(OrderTest):
) )
def test_po_cancel(self): def test_po_cancel(self):
""" """Test the PurchaseOrderCancel API endpoint"""
Test the PurchaseOrderCancel API endpoint
"""
po = models.PurchaseOrder.objects.get(pk=1) po = models.PurchaseOrder.objects.get(pk=1)
self.assertEqual(po.status, PurchaseOrderStatus.PENDING) self.assertEqual(po.status, PurchaseOrderStatus.PENDING)
@ -270,7 +251,6 @@ class PurchaseOrderTest(OrderTest):
def test_po_complete(self): def test_po_complete(self):
""" Test the PurchaseOrderComplete API endpoint """ """ Test the PurchaseOrderComplete API endpoint """
po = models.PurchaseOrder.objects.get(pk=3) po = models.PurchaseOrder.objects.get(pk=3)
url = reverse('api-po-complete', kwargs={'pk': po.pk}) url = reverse('api-po-complete', kwargs={'pk': po.pk})
@ -290,7 +270,6 @@ class PurchaseOrderTest(OrderTest):
def test_po_issue(self): def test_po_issue(self):
""" Test the PurchaseOrderIssue API endpoint """ """ Test the PurchaseOrderIssue API endpoint """
po = models.PurchaseOrder.objects.get(pk=2) po = models.PurchaseOrder.objects.get(pk=2)
url = reverse('api-po-issue', kwargs={'pk': po.pk}) url = reverse('api-po-issue', kwargs={'pk': po.pk})
@ -395,9 +374,7 @@ class PurchaseOrderDownloadTest(OrderTest):
class PurchaseOrderReceiveTest(OrderTest): class PurchaseOrderReceiveTest(OrderTest):
""" """Unit tests for receiving items against a PurchaseOrder"""
Unit tests for receiving items against a PurchaseOrder
"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -415,10 +392,7 @@ class PurchaseOrderReceiveTest(OrderTest):
order.save() order.save()
def test_empty(self): def test_empty(self):
""" """Test without any POST data"""
Test without any POST data
"""
data = self.post(self.url, {}, expected_code=400).data data = self.post(self.url, {}, expected_code=400).data
self.assertIn('This field is required', str(data['items'])) self.assertIn('This field is required', str(data['items']))
@ -428,10 +402,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(self.n, StockItem.objects.count()) self.assertEqual(self.n, StockItem.objects.count())
def test_no_items(self): def test_no_items(self):
""" """Test with an empty list of items"""
Test with an empty list of items
"""
data = self.post( data = self.post(
self.url, self.url,
{ {
@ -447,10 +418,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(self.n, StockItem.objects.count()) self.assertEqual(self.n, StockItem.objects.count())
def test_invalid_items(self): 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( data = self.post(
self.url, self.url,
{ {
@ -473,10 +441,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(self.n, StockItem.objects.count()) self.assertEqual(self.n, StockItem.objects.count())
def test_invalid_status(self): def test_invalid_status(self):
""" """Test with an invalid StockStatus value"""
Test with an invalid StockStatus value
"""
data = self.post( data = self.post(
self.url, self.url,
{ {
@ -498,10 +463,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(self.n, StockItem.objects.count()) self.assertEqual(self.n, StockItem.objects.count())
def test_mismatched_items(self): 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( data = self.post(
self.url, self.url,
{ {
@ -523,10 +485,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(self.n, StockItem.objects.count()) self.assertEqual(self.n, StockItem.objects.count())
def test_null_barcode(self): 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 # Set stock item barcode
item = StockItem.objects.get(pk=1) item = StockItem.objects.get(pk=1)
item.save() item.save()
@ -548,13 +507,11 @@ class PurchaseOrderReceiveTest(OrderTest):
) )
def test_invalid_barcodes(self): 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 - Cannot check in "duplicate" barcodes
- Barcodes cannot match UID field for existing StockItem - Barcodes cannot match UID field for existing StockItem
""" """
# Set stock item barcode # Set stock item barcode
item = StockItem.objects.get(pk=1) item = StockItem.objects.get(pk=1)
item.uid = 'MY-BARCODE-HASH' item.uid = 'MY-BARCODE-HASH'
@ -603,10 +560,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(self.n, StockItem.objects.count()) self.assertEqual(self.n, StockItem.objects.count())
def test_valid(self): def test_valid(self):
""" """Test receipt of valid data"""
Test receipt of valid data
"""
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1) line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2) 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()) self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
def test_batch_code(self): 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_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2) line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
@ -727,10 +678,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(item_2.batch, 'xyz-789') self.assertEqual(item_2.batch, 'xyz-789')
def test_serial_numbers(self): 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_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2) line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
@ -786,9 +734,7 @@ class PurchaseOrderReceiveTest(OrderTest):
class SalesOrderTest(OrderTest): class SalesOrderTest(OrderTest):
""" """Tests for the SalesOrder API"""
Tests for the SalesOrder API
"""
LIST_URL = reverse('api-so-list') LIST_URL = reverse('api-so-list')
@ -843,10 +789,7 @@ class SalesOrderTest(OrderTest):
self.get(url) self.get(url)
def test_so_operations(self): 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() n = models.SalesOrder.objects.count()
url = reverse('api-so-list') url = reverse('api-so-list')
@ -926,10 +869,7 @@ class SalesOrderTest(OrderTest):
response = self.get(url, expected_code=404) response = self.get(url, expected_code=404)
def test_so_create(self): 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.assignRole('sales_order.add')
self.post( self.post(
@ -980,9 +920,7 @@ class SalesOrderTest(OrderTest):
class SalesOrderLineItemTest(OrderTest): class SalesOrderLineItemTest(OrderTest):
""" """Tests for the SalesOrderLineItem API"""
Tests for the SalesOrderLineItem API
"""
def setUp(self): def setUp(self):
@ -1064,7 +1002,6 @@ class SalesOrderDownloadTest(OrderTest):
def test_download_fail(self): def test_download_fail(self):
"""Test that downloading without the 'export' option fails""" """Test that downloading without the 'export' option fails"""
url = reverse('api-so-list') url = reverse('api-so-list')
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -1151,9 +1088,7 @@ class SalesOrderDownloadTest(OrderTest):
class SalesOrderAllocateTest(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): def setUp(self):
super().setUp() super().setUp()
@ -1188,10 +1123,7 @@ class SalesOrderAllocateTest(OrderTest):
) )
def test_invalid(self): def test_invalid(self):
""" """Test POST with invalid data"""
Test POST with invalid data
"""
# No data # No data
response = self.post(self.url, {}, expected_code=400) 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'])) self.assertIn('Shipment is not associated with this order', str(response.data['shipment']))
def test_allocate(self): 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 # First, check that there are no line items allocated against this SalesOrder
self.assertEqual(self.order.stock_allocations.count(), 0) self.assertEqual(self.order.stock_allocations.count(), 0)
@ -1279,7 +1207,6 @@ class SalesOrderAllocateTest(OrderTest):
def test_shipment_complete(self): def test_shipment_complete(self):
"""Test that we can complete a shipment via the API""" """Test that we can complete a shipment via the API"""
url = reverse('api-so-shipment-ship', kwargs={'pk': self.shipment.pk}) url = reverse('api-so-shipment-ship', kwargs={'pk': self.shipment.pk})
self.assertFalse(self.shipment.is_complete()) self.assertFalse(self.shipment.is_complete())

View File

@ -1,6 +1,4 @@
""" """Unit tests for the 'order' model data migrations"""
Unit tests for the 'order' model data migrations
"""
from django_test_migrations.contrib.unittest_case import MigratorTestCase from django_test_migrations.contrib.unittest_case import MigratorTestCase
@ -8,18 +6,13 @@ from InvenTree.status_codes import SalesOrderStatus
class TestRefIntMigrations(MigratorTestCase): class TestRefIntMigrations(MigratorTestCase):
""" """Test entire schema migration"""
Test entire schema migration
"""
migrate_from = ('order', '0040_salesorder_target_date') migrate_from = ('order', '0040_salesorder_target_date')
migrate_to = ('order', '0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339') migrate_to = ('order', '0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339')
def prepare(self): def prepare(self):
""" """Create initial data set"""
Create initial data set
"""
# Create a purchase order from a supplier # Create a purchase order from a supplier
Company = self.old_state.apps.get_model('company', 'company') Company = self.old_state.apps.get_model('company', 'company')
@ -57,10 +50,7 @@ class TestRefIntMigrations(MigratorTestCase):
print(sales_order.reference_int) print(sales_order.reference_int)
def test_ref_field(self): 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') PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
SalesOrder = self.new_state.apps.get_model('order', 'salesorder') SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
@ -75,18 +65,13 @@ class TestRefIntMigrations(MigratorTestCase):
class TestShipmentMigration(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_from = ('order', '0051_auto_20211014_0623')
migrate_to = ('order', '0055_auto_20211025_0645') migrate_to = ('order', '0055_auto_20211025_0645')
def prepare(self): def prepare(self):
""" """Create an initial SalesOrder"""
Create an initial SalesOrder
"""
Company = self.old_state.apps.get_model('company', 'company') Company = self.old_state.apps.get_model('company', 'company')
customer = Company.objects.create( customer = Company.objects.create(
@ -112,10 +97,7 @@ class TestShipmentMigration(MigratorTestCase):
self.old_state.apps.get_model('order', 'salesordershipment') self.old_state.apps.get_model('order', 'salesordershipment')
def test_shipment_creation(self): 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') SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
Shipment = self.new_state.apps.get_model('order', 'salesordershipment') Shipment = self.new_state.apps.get_model('order', 'salesordershipment')
@ -125,18 +107,13 @@ class TestShipmentMigration(MigratorTestCase):
class TestAdditionalLineMigration(MigratorTestCase): class TestAdditionalLineMigration(MigratorTestCase):
""" """Test entire schema migration"""
Test entire schema migration
"""
migrate_from = ('order', '0063_alter_purchaseorderlineitem_unique_together') migrate_from = ('order', '0063_alter_purchaseorderlineitem_unique_together')
migrate_to = ('order', '0064_purchaseorderextraline_salesorderextraline') migrate_to = ('order', '0064_purchaseorderextraline_salesorderextraline')
def prepare(self): def prepare(self):
""" """Create initial data set"""
Create initial data set
"""
# Create a purchase order from a supplier # Create a purchase order from a supplier
Company = self.old_state.apps.get_model('company', 'company') Company = self.old_state.apps.get_model('company', 'company')
PurchaseOrder = self.old_state.apps.get_model('order', 'purchaseorder') PurchaseOrder = self.old_state.apps.get_model('order', 'purchaseorder')
@ -199,10 +176,7 @@ class TestAdditionalLineMigration(MigratorTestCase):
# ) # )
def test_po_migration(self): 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') PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
for ii in range(10): for ii in range(10):

View File

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

View File

@ -1,4 +1,4 @@
""" Unit tests for Order views (see views.py) """ """Unit tests for Order views (see views.py)"""
from django.urls import reverse from django.urls import reverse
@ -37,7 +37,7 @@ class OrderListTest(OrderViewTestCase):
class PurchaseOrderTests(OrderViewTestCase): class PurchaseOrderTests(OrderViewTestCase):
""" Tests for PurchaseOrder views """ """Tests for PurchaseOrder views"""
def test_detail_view(self): def test_detail_view(self):
""" Retrieve PO detail view """ """ Retrieve PO detail view """
@ -47,8 +47,7 @@ class PurchaseOrderTests(OrderViewTestCase):
self.assertIn('PurchaseOrderStatus', keys) self.assertIn('PurchaseOrderStatus', keys)
def test_po_export(self): def test_po_export(self):
""" Export PurchaseOrder """ """Export PurchaseOrder"""
response = self.client.get(reverse('po-export', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('po-export', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Response should be streaming-content (file download) # Response should be streaming-content (file download)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta from datetime import datetime, timedelta
import django.core.exceptions as django_exceptions import django.core.exceptions as django_exceptions
@ -14,9 +12,7 @@ from .models import PurchaseOrder, PurchaseOrderLineItem
class OrderTest(TestCase): class OrderTest(TestCase):
""" """Tests to ensure that the order models are functioning correctly."""
Tests to ensure that the order models are functioning correctly.
"""
fixtures = [ fixtures = [
'company', 'company',
@ -30,8 +26,7 @@ class OrderTest(TestCase):
] ]
def test_basics(self): def test_basics(self):
""" Basic tests e.g. repr functions etc """ """Basic tests e.g. repr functions etc"""
order = PurchaseOrder.objects.get(pk=1) order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/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)") self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
def test_overdue(self): def test_overdue(self):
""" """Test overdue status functionality"""
Test overdue status functionality
"""
today = datetime.now().date() today = datetime.now().date()
order = PurchaseOrder.objects.get(pk=1) order = PurchaseOrder.objects.get(pk=1)
@ -61,8 +53,7 @@ class OrderTest(TestCase):
self.assertFalse(order.is_overdue) self.assertFalse(order.is_overdue)
def test_on_order(self): 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') part = Part.objects.get(name='M2x4 LPHS')
open_orders = [] open_orders = []
@ -76,8 +67,7 @@ class OrderTest(TestCase):
self.assertEqual(part.on_order, 1400) self.assertEqual(part.on_order, 1400)
def test_add_items(self): 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) order = PurchaseOrder.objects.get(pk=1)
self.assertEqual(order.status, PurchaseOrderStatus.PENDING) self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
@ -113,8 +103,7 @@ class OrderTest(TestCase):
order.add_line_item(sku, 99) order.add_line_item(sku, 99)
def test_pricing(self): 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) order = PurchaseOrder.objects.get(pk=7)
self.assertEqual(order.status, PurchaseOrderStatus.PENDING) self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
@ -146,8 +135,7 @@ class OrderTest(TestCase):
self.assertEqual(order.lines.first().purchase_price.amount, 1.25) self.assertEqual(order.lines.first().purchase_price.amount, 1.25)
def test_receive(self): def test_receive(self):
""" Test order receiving functions """ """Test order receiving functions"""
part = Part.objects.get(name='M2x4 LPHS') part = Part.objects.get(name='M2x4 LPHS')
# Receive some items # Receive some items

View File

@ -1,5 +1,4 @@
""" """URL lookup for the Order app. Provides URL endpoints for:
URL lookup for the Order app. Provides URL endpoints for:
- List view of Purchase Orders - List view of Purchase Orders
- Detail view of Purchase Orders - Detail view of Purchase Orders

View File

@ -1,6 +1,4 @@
""" """Django views for interacting with Order app"""
Django views for interacting with Order app
"""
import logging import logging
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
@ -33,7 +31,7 @@ logger = logging.getLogger("inventree")
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView): class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
""" List view for all purchase orders """ """List view for all purchase orders"""
model = PurchaseOrder model = PurchaseOrder
template_name = 'order/purchase_orders.html' template_name = 'order/purchase_orders.html'
@ -61,7 +59,7 @@ class SalesOrderIndex(InvenTreeRoleMixin, ListView):
class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
""" Detail view for a PurchaseOrder object """ """Detail view for a PurchaseOrder object"""
context_object_name = 'order' context_object_name = 'order'
queryset = PurchaseOrder.objects.all().prefetch_related('lines') queryset = PurchaseOrder.objects.all().prefetch_related('lines')
@ -74,7 +72,7 @@ class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
""" Detail view for a SalesOrder object """ """Detail view for a SalesOrder object"""
context_object_name = 'order' context_object_name = 'order'
queryset = SalesOrder.objects.all().prefetch_related('lines__allocations__item__purchase_order') queryset = SalesOrder.objects.all().prefetch_related('lines__allocations__item__purchase_order')
@ -82,7 +80,7 @@ class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView)
class PurchaseOrderUpload(FileManagementFormView): 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): class OrderFileManager(FileManager):
REQUIRED_HEADERS = [ REQUIRED_HEADERS = [
@ -126,12 +124,12 @@ class PurchaseOrderUpload(FileManagementFormView):
file_manager_class = OrderFileManager file_manager_class = OrderFileManager
def get_order(self): def get_order(self):
""" Get order or return 404 """ """Get order or return 404"""
return get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) return get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
def get_context_data(self, form, **kwargs): 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) context = super().get_context_data(form=form, **kwargs)
@ -142,11 +140,11 @@ class PurchaseOrderUpload(FileManagementFormView):
return context return context
def get_field_selection(self): 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. This function is called once the field selection has been validated.
The pre-fill data are then passed through to the SupplierPart selection form. The pre-fill data are then passed through to the SupplierPart selection form.
""" """
order = self.get_order() order = self.get_order()
self.allowed_items = SupplierPart.objects.filter(supplier=order.supplier).prefetch_related('manufacturer_part') self.allowed_items = SupplierPart.objects.filter(supplier=order.supplier).prefetch_related('manufacturer_part')
@ -231,8 +229,7 @@ class PurchaseOrderUpload(FileManagementFormView):
row['notes'] = notes row['notes'] = notes
def done(self, form_list, **kwargs): 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() order = self.get_order()
items = self.get_clean_items() items = self.get_clean_items()
@ -263,8 +260,7 @@ class PurchaseOrderUpload(FileManagementFormView):
class SalesOrderExport(AjaxView): 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 - File format can optionally be passed as a query parameter e.g. ?format=CSV
- Default file format is CSV - Default file format is CSV
@ -290,7 +286,7 @@ class SalesOrderExport(AjaxView):
class PurchaseOrderExport(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 - File format can be optionally passed as a query param e.g. ?format=CSV
- Default file format is CSV - Default file format is CSV
@ -321,7 +317,7 @@ class PurchaseOrderExport(AjaxView):
class LineItemPricing(PartPricing): class LineItemPricing(PartPricing):
""" View for inspecting part pricing information """ """View for inspecting part pricing information"""
class EnhancedForm(PartPricing.form_class): class EnhancedForm(PartPricing.form_class):
pk = IntegerField(widget=HiddenInput()) pk = IntegerField(widget=HiddenInput())
@ -365,7 +361,7 @@ class LineItemPricing(PartPricing):
return None return None
def get_quantity(self): def get_quantity(self):
""" Return set quantity in decimal format """ """Return set quantity in decimal format"""
qty = Decimal(self.request.GET.get('quantity', 1)) qty = Decimal(self.request.GET.get('quantity', 1))
if qty == 1: if qty == 1:
return Decimal(self.request.POST.get('quantity', 1)) return Decimal(self.request.POST.get('quantity', 1))

View File

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

View File

@ -12,21 +12,15 @@ class PartConfig(AppConfig):
name = 'part' name = 'part'
def ready(self): 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(): if canAppAccessDatabase():
self.update_trackable_status() self.update_trackable_status()
def update_trackable_status(self): 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. In such a case, force the top-level part to be trackable too.
""" """
from .models import BomItem from .models import BomItem
try: try:

View File

@ -1,5 +1,4 @@
""" """Functionality for Bill of Material (BOM) management.
Functionality for Bill of Material (BOM) management.
Primarily BOM upload tools. Primarily BOM upload tools.
""" """
@ -15,14 +14,12 @@ from .models import BomItem
def IsValidBOMFormat(fmt): 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() return fmt.strip().lower() in GetExportFormats()
def MakeBomTemplate(fmt): 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() fmt = fmt.strip().lower()
if not IsValidBOMFormat(fmt): 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): 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: Args:
fmt: File format (default = 'csv') fmt: File format (default = 'csv')
cascade: If True, multi-level BOM output is supported. Otherwise, a flat top-level-only BOM is exported. cascade: If True, multi-level BOM output is supported. Otherwise, a flat top-level-only BOM is exported.
""" """
if not IsValidBOMFormat(fmt): if not IsValidBOMFormat(fmt):
fmt = 'csv' fmt = 'csv'

View File

@ -1,6 +1,4 @@
""" """Django Forms for interacting with Part objects"""
Django Forms for interacting with Part objects
"""
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -19,7 +17,7 @@ from .models import (Part, PartCategory, PartCategoryParameterTemplate,
class PartModelChoiceField(forms.ModelChoiceField): 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): def label_from_instance(self, part):
@ -33,9 +31,7 @@ class PartModelChoiceField(forms.ModelChoiceField):
class PartImageDownloadForm(HelperForm): class PartImageDownloadForm(HelperForm):
""" """Form for downloading an image from a URL"""
Form for downloading an image from a URL
"""
url = forms.URLField( url = forms.URLField(
label=_('URL'), label=_('URL'),
@ -51,10 +47,10 @@ class PartImageDownloadForm(HelperForm):
class BomMatchItemForm(MatchItemForm): class BomMatchItemForm(MatchItemForm):
""" Override MatchItemForm fields """ """Override MatchItemForm fields"""
def get_special_field(self, col_guess, row, file_manager): def get_special_field(self, col_guess, row, file_manager):
""" Set special fields """ """Set special fields"""
# set quantity field # set quantity field
if 'quantity' in col_guess.lower(): if 'quantity' in col_guess.lower():
@ -74,13 +70,13 @@ class BomMatchItemForm(MatchItemForm):
class SetPartCategoryForm(forms.Form): 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')) part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
class EditPartParameterTemplateForm(HelperForm): class EditPartParameterTemplateForm(HelperForm):
""" Form for editing a PartParameterTemplate object """ """Form for editing a PartParameterTemplate object"""
class Meta: class Meta:
model = PartParameterTemplate model = PartParameterTemplate
@ -91,7 +87,7 @@ class EditPartParameterTemplateForm(HelperForm):
class EditCategoryParameterTemplateForm(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, add_to_same_level_categories = forms.BooleanField(required=False,
initial=False, initial=False,
@ -113,7 +109,7 @@ class EditCategoryParameterTemplateForm(HelperForm):
class PartPriceForm(forms.Form): class PartPriceForm(forms.Form):
""" Simple form for viewing part pricing information """ """Simple form for viewing part pricing information"""
quantity = forms.IntegerField( quantity = forms.IntegerField(
required=True, required=True,
@ -130,9 +126,7 @@ class PartPriceForm(forms.Form):
class EditPartSalePriceBreakForm(HelperForm): 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')) quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
@ -146,9 +140,7 @@ class EditPartSalePriceBreakForm(HelperForm):
class EditPartInternalPriceBreakForm(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')) quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))

File diff suppressed because it is too large Load Diff