mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
docstring adjustments
This commit is contained in:
parent
60f13ad2e8
commit
1e90900918
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Company database model definitions
|
||||
"""
|
||||
"""Company database model definitions"""
|
||||
|
||||
import os
|
||||
|
||||
@ -27,7 +25,7 @@ from InvenTree.status_codes import PurchaseOrderStatus
|
||||
|
||||
|
||||
def rename_company_image(instance, filename):
|
||||
""" Function to rename a company image after upload
|
||||
"""Function to rename a company image after upload
|
||||
|
||||
Args:
|
||||
instance: Company object
|
||||
@ -36,7 +34,6 @@ def rename_company_image(instance, filename):
|
||||
Returns:
|
||||
New image filename
|
||||
"""
|
||||
|
||||
base = 'company_images'
|
||||
|
||||
if filename.count('.') > 0:
|
||||
@ -54,6 +51,7 @@ def rename_company_image(instance, filename):
|
||||
|
||||
class Company(models.Model):
|
||||
""" A Company object represents an external company.
|
||||
|
||||
It may be a supplier or a customer or a manufacturer (or a combination)
|
||||
|
||||
- A supplier is a company from which parts can be purchased
|
||||
@ -156,7 +154,6 @@ class Company(models.Model):
|
||||
- If the currency code is invalid, use the default currency
|
||||
- If the currency code is not specified, use the default currency
|
||||
"""
|
||||
|
||||
code = self.currency
|
||||
|
||||
if code not in CURRENCIES:
|
||||
@ -174,7 +171,6 @@ class Company(models.Model):
|
||||
|
||||
def get_image_url(self):
|
||||
""" Return the URL of the image for this company """
|
||||
|
||||
if self.image:
|
||||
return getMediaUrl(self.image.url)
|
||||
else:
|
||||
@ -182,7 +178,6 @@ class Company(models.Model):
|
||||
|
||||
def get_thumbnail_url(self):
|
||||
""" Return the URL for the thumbnail image for this Company """
|
||||
|
||||
if self.image:
|
||||
return getMediaUrl(self.image.thumbnail.url)
|
||||
else:
|
||||
@ -247,7 +242,6 @@ class Company(models.Model):
|
||||
- Failed / lost
|
||||
- Returned
|
||||
"""
|
||||
|
||||
return self.purchase_orders.exclude(status__in=PurchaseOrderStatus.OPEN)
|
||||
|
||||
def complete_purchase_orders(self):
|
||||
@ -255,7 +249,6 @@ class Company(models.Model):
|
||||
|
||||
def failed_purchase_orders(self):
|
||||
""" Return any purchase orders which were not successful """
|
||||
|
||||
return self.purchase_orders.filter(status__in=PurchaseOrderStatus.FAILED)
|
||||
|
||||
|
||||
@ -346,10 +339,7 @@ class ManufacturerPart(models.Model):
|
||||
|
||||
@classmethod
|
||||
def create(cls, part, manufacturer, mpn, description, link=None):
|
||||
""" Check if ManufacturerPart instance does not already exist
|
||||
then create it
|
||||
"""
|
||||
|
||||
"""Check if ManufacturerPart instance does not already exist then create it"""
|
||||
manufacturer_part = None
|
||||
|
||||
try:
|
||||
@ -509,7 +499,6 @@ class SupplierPart(models.Model):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Overriding save method to connect an existing ManufacturerPart """
|
||||
|
||||
manufacturer_part = None
|
||||
|
||||
if all(key in kwargs for key in ('manufacturer', 'MPN')):
|
||||
@ -593,10 +582,10 @@ class SupplierPart(models.Model):
|
||||
|
||||
@property
|
||||
def manufacturer_string(self):
|
||||
""" Format a MPN string for this SupplierPart.
|
||||
"""Format a MPN string for this SupplierPart.
|
||||
|
||||
Concatenates manufacture name and part number.
|
||||
"""
|
||||
|
||||
items = []
|
||||
|
||||
if self.manufacturer_part:
|
||||
@ -621,14 +610,12 @@ class SupplierPart(models.Model):
|
||||
return self.get_price(1)
|
||||
|
||||
def add_price_break(self, quantity, price):
|
||||
"""
|
||||
Create a new price break for this part
|
||||
"""Create a new price break for this part
|
||||
|
||||
args:
|
||||
quantity - Numerical quantity
|
||||
price - Must be a Money object
|
||||
"""
|
||||
|
||||
# Check if a price break at that quantity already exists...
|
||||
if self.price_breaks.filter(quantity=quantity, part=self.pk).exists():
|
||||
return
|
||||
@ -642,18 +629,14 @@ class SupplierPart(models.Model):
|
||||
get_price = common.models.get_price
|
||||
|
||||
def open_orders(self):
|
||||
""" Return a database query for PurchaseOrder line items for this SupplierPart,
|
||||
limited to purchase orders that are open / outstanding.
|
||||
"""
|
||||
|
||||
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding."""
|
||||
return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatus.OPEN)
|
||||
|
||||
def on_order(self):
|
||||
""" Return the total quantity of items currently on order.
|
||||
"""Return the total quantity of items currently on order.
|
||||
|
||||
Subtract partially received stock as appropriate
|
||||
"""
|
||||
|
||||
totals = self.open_orders().aggregate(Sum('quantity'), Sum('received'))
|
||||
|
||||
# Quantity on order
|
||||
@ -668,8 +651,7 @@ class SupplierPart(models.Model):
|
||||
return max(q - r, 0)
|
||||
|
||||
def purchase_orders(self):
|
||||
""" Returns a list of purchase orders relating to this supplier part """
|
||||
|
||||
"""Returns a list of purchase orders relating to this supplier part"""
|
||||
return [line.order for line in self.purchase_order_line_items.all().prefetch_related('order')]
|
||||
|
||||
@property
|
||||
@ -692,7 +674,7 @@ class SupplierPart(models.Model):
|
||||
|
||||
|
||||
class SupplierPriceBreak(common.models.PriceBreak):
|
||||
""" Represents a quantity price break for a SupplierPart.
|
||||
"""Represents a quantity price break for a SupplierPart.
|
||||
- Suppliers can offer discounts at larger quantities
|
||||
- SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s)
|
||||
|
||||
|
@ -1,3 +1 @@
|
||||
"""
|
||||
The Order module is responsible for managing Orders
|
||||
"""
|
||||
"""The Order module is responsible for managing Orders"""
|
||||
|
@ -1,8 +1,4 @@
|
||||
"""
|
||||
Order model definitions
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Order model definitions"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
@ -47,10 +43,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def get_next_po_number():
|
||||
"""
|
||||
Returns the next available PurchaseOrder reference number
|
||||
"""
|
||||
|
||||
"""Returns the next available PurchaseOrder reference number"""
|
||||
if PurchaseOrder.objects.count() == 0:
|
||||
return '0001'
|
||||
|
||||
@ -76,10 +69,7 @@ def get_next_po_number():
|
||||
|
||||
|
||||
def get_next_so_number():
|
||||
"""
|
||||
Returns the next available SalesOrder reference number
|
||||
"""
|
||||
|
||||
"""Returns the next available SalesOrder reference number"""
|
||||
if SalesOrder.objects.count() == 0:
|
||||
return '0001'
|
||||
|
||||
@ -105,7 +95,7 @@ def get_next_so_number():
|
||||
|
||||
|
||||
class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
""" Abstract model for an order.
|
||||
"""Abstract model for an order.
|
||||
|
||||
Instances of this class:
|
||||
|
||||
@ -159,15 +149,13 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
|
||||
|
||||
def get_total_price(self, target_currency=currency_code_default()):
|
||||
"""
|
||||
Calculates the total price of all order lines, and converts to the specified target currency.
|
||||
"""Calculates the total price of all order lines, and converts to the specified target currency.
|
||||
|
||||
If not specified, the default system currency is used.
|
||||
|
||||
If currency conversion fails (e.g. there are no valid conversion rates),
|
||||
then we simply return zero, rather than attempting some other calculation.
|
||||
"""
|
||||
|
||||
total = Money(0, target_currency)
|
||||
|
||||
# gather name reference
|
||||
@ -230,7 +218,7 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
|
||||
|
||||
|
||||
class PurchaseOrder(Order):
|
||||
""" A PurchaseOrder represents goods shipped inwards from an external supplier.
|
||||
"""A PurchaseOrder represents goods shipped inwards from an external supplier.
|
||||
|
||||
Attributes:
|
||||
supplier: Reference to the company supplying the goods in the order
|
||||
@ -247,8 +235,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
@staticmethod
|
||||
def filterByDate(queryset, min_date, max_date):
|
||||
"""
|
||||
Filter by 'minimum and maximum date range'
|
||||
"""Filter by 'minimum and maximum date range'
|
||||
|
||||
- Specified as min_date, max_date
|
||||
- Both must be specified for filter to be applied
|
||||
@ -259,7 +246,6 @@ class PurchaseOrder(Order):
|
||||
- A "pending" order where the target date lies within the date range
|
||||
- TODO: An "overdue" order where the target date is in the past
|
||||
"""
|
||||
|
||||
date_fmt = '%Y-%m-%d' # ISO format date string
|
||||
|
||||
# Ensure that both dates are valid
|
||||
@ -344,7 +330,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def add_line_item(self, supplier_part, quantity, group=True, reference='', purchase_price=None):
|
||||
""" Add a new line item to this purchase order.
|
||||
"""Add a new line item to this purchase order.
|
||||
This function will check that:
|
||||
|
||||
* The supplier part matches the supplier specified for this purchase order
|
||||
@ -355,7 +341,6 @@ class PurchaseOrder(Order):
|
||||
quantity - The number of items to add
|
||||
group - If True, this new quantity will be added to an existing line item for the same supplier_part (if it exists)
|
||||
"""
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
if quantity <= 0:
|
||||
@ -396,8 +381,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def place_order(self):
|
||||
""" Marks the PurchaseOrder as PLACED. Order must be currently PENDING. """
|
||||
|
||||
"""Marks the PurchaseOrder as PLACED. Order must be currently PENDING."""
|
||||
if self.status == PurchaseOrderStatus.PENDING:
|
||||
self.status = PurchaseOrderStatus.PLACED
|
||||
self.issue_date = datetime.now().date()
|
||||
@ -407,8 +391,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def complete_order(self):
|
||||
""" Marks the PurchaseOrder as COMPLETE. Order must be currently PLACED. """
|
||||
|
||||
"""Marks the PurchaseOrder as COMPLETE. Order must be currently PLACED."""
|
||||
if self.status == PurchaseOrderStatus.PLACED:
|
||||
self.status = PurchaseOrderStatus.COMPLETE
|
||||
self.complete_date = datetime.now().date()
|
||||
@ -418,22 +401,17 @@ class PurchaseOrder(Order):
|
||||
|
||||
@property
|
||||
def is_overdue(self):
|
||||
"""
|
||||
Returns True if this PurchaseOrder is "overdue"
|
||||
"""Returns True if this PurchaseOrder is "overdue"
|
||||
|
||||
Makes use of the OVERDUE_FILTER to avoid code duplication.
|
||||
"""
|
||||
|
||||
query = PurchaseOrder.objects.filter(pk=self.pk)
|
||||
query = query.filter(PurchaseOrder.OVERDUE_FILTER)
|
||||
|
||||
return query.exists()
|
||||
|
||||
def can_cancel(self):
|
||||
"""
|
||||
A PurchaseOrder can only be cancelled under the following circumstances:
|
||||
"""
|
||||
|
||||
"""A PurchaseOrder can only be cancelled under the following circumstances:"""
|
||||
return self.status in [
|
||||
PurchaseOrderStatus.PLACED,
|
||||
PurchaseOrderStatus.PENDING
|
||||
@ -441,8 +419,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_order(self):
|
||||
""" Marks the PurchaseOrder as CANCELLED. """
|
||||
|
||||
"""Marks the PurchaseOrder as CANCELLED."""
|
||||
if self.can_cancel():
|
||||
self.status = PurchaseOrderStatus.CANCELLED
|
||||
self.save()
|
||||
@ -450,16 +427,14 @@ class PurchaseOrder(Order):
|
||||
trigger_event('purchaseorder.cancelled', id=self.pk)
|
||||
|
||||
def pending_line_items(self):
|
||||
""" Return a list of pending line items for this order.
|
||||
"""Return a list of pending line items for this order.
|
||||
|
||||
Any line item where 'received' < 'quantity' will be returned.
|
||||
"""
|
||||
|
||||
return self.lines.filter(quantity__gt=F('received'))
|
||||
|
||||
def completed_line_items(self):
|
||||
"""
|
||||
Return a list of completed line items against this order
|
||||
"""
|
||||
"""Return a list of completed line items against this order"""
|
||||
return self.lines.filter(quantity__lte=F('received'))
|
||||
|
||||
@property
|
||||
@ -477,16 +452,12 @@ class PurchaseOrder(Order):
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
""" Return True if all line items have been received """
|
||||
|
||||
"""Return True if all line items have been received"""
|
||||
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
||||
|
||||
@transaction.atomic
|
||||
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs):
|
||||
"""
|
||||
Receive a line item (or partial line item) against this PurchaseOrder
|
||||
"""
|
||||
|
||||
"""Receive a line item (or partial line item) against this PurchaseOrder"""
|
||||
# Extract optional batch code for the new stock item
|
||||
batch_code = kwargs.get('batch_code', '')
|
||||
|
||||
@ -573,8 +544,7 @@ class PurchaseOrder(Order):
|
||||
|
||||
|
||||
class SalesOrder(Order):
|
||||
"""
|
||||
A SalesOrder represents a list of goods shipped outwards to a customer.
|
||||
"""A SalesOrder represents a list of goods shipped outwards to a customer.
|
||||
|
||||
Attributes:
|
||||
customer: Reference to the company receiving the goods in the order
|
||||
@ -590,8 +560,7 @@ class SalesOrder(Order):
|
||||
|
||||
@staticmethod
|
||||
def filterByDate(queryset, min_date, max_date):
|
||||
"""
|
||||
Filter by "minimum and maximum date range"
|
||||
"""Filter by "minimum and maximum date range"
|
||||
|
||||
- Specified as min_date, max_date
|
||||
- Both must be specified for filter to be applied
|
||||
@ -602,7 +571,6 @@ class SalesOrder(Order):
|
||||
- A "pending" order where the target date lies within the date range
|
||||
- TODO: An "overdue" order where the target date is in the past
|
||||
"""
|
||||
|
||||
date_fmt = '%Y-%m-%d' # ISO format date string
|
||||
|
||||
# Ensure that both dates are valid
|
||||
@ -682,12 +650,10 @@ class SalesOrder(Order):
|
||||
|
||||
@property
|
||||
def is_overdue(self):
|
||||
"""
|
||||
Returns true if this SalesOrder is "overdue":
|
||||
"""Returns true if this SalesOrder is "overdue":
|
||||
|
||||
Makes use of the OVERDUE_FILTER to avoid code duplication.
|
||||
"""
|
||||
|
||||
query = SalesOrder.objects.filter(pk=self.pk)
|
||||
query = query.filter(SalesOrder.OVERDUE_FILTER)
|
||||
|
||||
@ -699,17 +665,13 @@ class SalesOrder(Order):
|
||||
|
||||
@property
|
||||
def stock_allocations(self):
|
||||
"""
|
||||
Return a queryset containing all allocations for this order
|
||||
"""
|
||||
|
||||
"""Return a queryset containing all allocations for this order"""
|
||||
return SalesOrderAllocation.objects.filter(
|
||||
line__in=[line.pk for line in self.lines.all()]
|
||||
)
|
||||
|
||||
def is_fully_allocated(self):
|
||||
""" Return True if all line items are fully allocated """
|
||||
|
||||
"""Return True if all line items are fully allocated"""
|
||||
for line in self.lines.all():
|
||||
if not line.is_fully_allocated():
|
||||
return False
|
||||
@ -717,8 +679,7 @@ class SalesOrder(Order):
|
||||
return True
|
||||
|
||||
def is_over_allocated(self):
|
||||
""" Return true if any lines in the order are over-allocated """
|
||||
|
||||
"""Return true if any lines in the order are over-allocated"""
|
||||
for line in self.lines.all():
|
||||
if line.is_over_allocated():
|
||||
return True
|
||||
@ -726,19 +687,14 @@ class SalesOrder(Order):
|
||||
return False
|
||||
|
||||
def is_completed(self):
|
||||
"""
|
||||
Check if this order is "shipped" (all line items delivered),
|
||||
"""
|
||||
|
||||
"""Check if this order is "shipped" (all line items delivered)"""
|
||||
return self.lines.count() > 0 and all([line.is_completed() for line in self.lines.all()])
|
||||
|
||||
def can_complete(self, raise_error=False):
|
||||
"""
|
||||
Test if this SalesOrder can be completed.
|
||||
"""Test if this SalesOrder can be completed.
|
||||
|
||||
Throws a ValidationError if cannot be completed.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
||||
# Order without line items cannot be completed
|
||||
@ -765,10 +721,7 @@ class SalesOrder(Order):
|
||||
return True
|
||||
|
||||
def complete_order(self, user):
|
||||
"""
|
||||
Mark this order as "complete"
|
||||
"""
|
||||
|
||||
"""Mark this order as "complete"""
|
||||
if not self.can_complete():
|
||||
return False
|
||||
|
||||
@ -783,10 +736,7 @@ class SalesOrder(Order):
|
||||
return True
|
||||
|
||||
def can_cancel(self):
|
||||
"""
|
||||
Return True if this order can be cancelled
|
||||
"""
|
||||
|
||||
"""Return True if this order can be cancelled"""
|
||||
if self.status != SalesOrderStatus.PENDING:
|
||||
return False
|
||||
|
||||
@ -794,13 +744,11 @@ class SalesOrder(Order):
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_order(self):
|
||||
"""
|
||||
Cancel this order (only if it is "pending")
|
||||
"""Cancel this order (only if it is "pending")
|
||||
|
||||
- Mark the order as 'cancelled'
|
||||
- Delete any StockItems which have been allocated
|
||||
"""
|
||||
|
||||
if not self.can_cancel():
|
||||
return False
|
||||
|
||||
@ -820,15 +768,11 @@ class SalesOrder(Order):
|
||||
return self.lines.count()
|
||||
|
||||
def completed_line_items(self):
|
||||
"""
|
||||
Return a queryset of the completed line items for this order
|
||||
"""
|
||||
"""Return a queryset of the completed line items for this order"""
|
||||
return self.lines.filter(shipped__gte=F('quantity'))
|
||||
|
||||
def pending_line_items(self):
|
||||
"""
|
||||
Return a queryset of the pending line items for this order
|
||||
"""
|
||||
"""Return a queryset of the pending line items for this order"""
|
||||
return self.lines.filter(shipped__lt=F('quantity'))
|
||||
|
||||
@property
|
||||
@ -840,16 +784,11 @@ class SalesOrder(Order):
|
||||
return self.pending_line_items().count()
|
||||
|
||||
def completed_shipments(self):
|
||||
"""
|
||||
Return a queryset of the completed shipments for this order
|
||||
"""
|
||||
"""Return a queryset of the completed shipments for this order"""
|
||||
return self.shipments.exclude(shipment_date=None)
|
||||
|
||||
def pending_shipments(self):
|
||||
"""
|
||||
Return a queryset of the pending shipments for this order
|
||||
"""
|
||||
|
||||
"""Return a queryset of the pending shipments for this order"""
|
||||
return self.shipments.filter(shipment_date=None)
|
||||
|
||||
@property
|
||||
@ -867,9 +806,7 @@ class SalesOrder(Order):
|
||||
|
||||
@receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log')
|
||||
def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs):
|
||||
"""
|
||||
Callback function to be executed after a SalesOrder instance is saved
|
||||
"""
|
||||
"""Callback function to be executed after a SalesOrder instance is saved"""
|
||||
if created and getSetting('SALESORDER_DEFAULT_SHIPMENT'):
|
||||
# A new SalesOrder has just been created
|
||||
|
||||
@ -881,9 +818,7 @@ def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs
|
||||
|
||||
|
||||
class PurchaseOrderAttachment(InvenTreeAttachment):
|
||||
"""
|
||||
Model for storing file attachments against a PurchaseOrder object
|
||||
"""
|
||||
"""Model for storing file attachments against a PurchaseOrder object"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
@ -896,9 +831,7 @@ class PurchaseOrderAttachment(InvenTreeAttachment):
|
||||
|
||||
|
||||
class SalesOrderAttachment(InvenTreeAttachment):
|
||||
"""
|
||||
Model for storing file attachments against a SalesOrder object
|
||||
"""
|
||||
"""Model for storing file attachments against a SalesOrder object"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
@ -911,7 +844,7 @@ class SalesOrderAttachment(InvenTreeAttachment):
|
||||
|
||||
|
||||
class OrderLineItem(models.Model):
|
||||
""" Abstract model for an order line item
|
||||
"""Abstract model for an order line item
|
||||
|
||||
Attributes:
|
||||
quantity: Number of items
|
||||
@ -951,8 +884,8 @@ class OrderLineItem(models.Model):
|
||||
|
||||
|
||||
class OrderExtraLine(OrderLineItem):
|
||||
"""
|
||||
Abstract Model for a single ExtraLine in a Order
|
||||
"""Abstract Model for a single ExtraLine in a Order
|
||||
|
||||
Attributes:
|
||||
price: The unit sale price for this OrderLineItem
|
||||
"""
|
||||
@ -984,7 +917,7 @@ class OrderExtraLine(OrderLineItem):
|
||||
|
||||
|
||||
class PurchaseOrderLineItem(OrderLineItem):
|
||||
""" Model for a purchase order line item.
|
||||
"""Model for a purchase order line item.
|
||||
|
||||
Attributes:
|
||||
order: Reference to a PurchaseOrder object
|
||||
@ -1024,8 +957,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
)
|
||||
|
||||
def get_base_part(self):
|
||||
"""
|
||||
Return the base part.Part object for the line item
|
||||
"""Return the base part.Part object for the line item
|
||||
|
||||
Note: Returns None if the SupplierPart is not set!
|
||||
"""
|
||||
@ -1067,14 +999,12 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
)
|
||||
|
||||
def get_destination(self):
|
||||
"""
|
||||
Show where the line item is or should be placed
|
||||
"""Show where the line item is or should be placed
|
||||
|
||||
NOTE: If a line item gets split when recieved, only an arbitrary
|
||||
stock items location will be reported as the location for the
|
||||
entire line.
|
||||
"""
|
||||
|
||||
for stock in stock_models.StockItem.objects.filter(supplier_part=self.part, purchase_order=self.order):
|
||||
if stock.location:
|
||||
return stock.location
|
||||
@ -1084,14 +1014,13 @@ class PurchaseOrderLineItem(OrderLineItem):
|
||||
return self.part.part.default_location
|
||||
|
||||
def remaining(self):
|
||||
""" Calculate the number of items remaining to be received """
|
||||
"""Calculate the number of items remaining to be received"""
|
||||
r = self.quantity - self.received
|
||||
return max(r, 0)
|
||||
|
||||
|
||||
class PurchaseOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
Model for a single ExtraLine in a PurchaseOrder
|
||||
"""Model for a single ExtraLine in a PurchaseOrder
|
||||
Attributes:
|
||||
order: Link to the PurchaseOrder that this line belongs to
|
||||
title: title of line
|
||||
@ -1105,8 +1034,7 @@ class PurchaseOrderExtraLine(OrderExtraLine):
|
||||
|
||||
|
||||
class SalesOrderLineItem(OrderLineItem):
|
||||
"""
|
||||
Model for a single LineItem in a SalesOrder
|
||||
"""Model for a single LineItem in a SalesOrder
|
||||
|
||||
Attributes:
|
||||
order: Link to the SalesOrder that this line item belongs to
|
||||
@ -1150,47 +1078,38 @@ class SalesOrderLineItem(OrderLineItem):
|
||||
]
|
||||
|
||||
def fulfilled_quantity(self):
|
||||
"""
|
||||
Return the total stock quantity fulfilled against this line item.
|
||||
"""
|
||||
|
||||
"""Return the total stock quantity fulfilled against this line item."""
|
||||
query = self.order.stock_items.filter(part=self.part).aggregate(fulfilled=Coalesce(Sum('quantity'), Decimal(0)))
|
||||
|
||||
return query['fulfilled']
|
||||
|
||||
def allocated_quantity(self):
|
||||
""" Return the total stock quantity allocated to this LineItem.
|
||||
"""Return the total stock quantity allocated to this LineItem.
|
||||
|
||||
This is a summation of the quantity of each attached StockItem
|
||||
"""
|
||||
|
||||
query = self.allocations.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0)))
|
||||
|
||||
return query['allocated']
|
||||
|
||||
def is_fully_allocated(self):
|
||||
""" Return True if this line item is fully allocated """
|
||||
|
||||
"""Return True if this line item is fully allocated"""
|
||||
if self.order.status == SalesOrderStatus.SHIPPED:
|
||||
return self.fulfilled_quantity() >= self.quantity
|
||||
|
||||
return self.allocated_quantity() >= self.quantity
|
||||
|
||||
def is_over_allocated(self):
|
||||
""" Return True if this line item is over allocated """
|
||||
"""Return True if this line item is over allocated"""
|
||||
return self.allocated_quantity() > self.quantity
|
||||
|
||||
def is_completed(self):
|
||||
"""
|
||||
Return True if this line item is completed (has been fully shipped)
|
||||
"""
|
||||
|
||||
"""Return True if this line item is completed (has been fully shipped)"""
|
||||
return self.shipped >= self.quantity
|
||||
|
||||
|
||||
class SalesOrderShipment(models.Model):
|
||||
"""
|
||||
The SalesOrderShipment model represents a physical shipment made against a SalesOrder.
|
||||
"""The SalesOrderShipment model represents a physical shipment made against a SalesOrder.
|
||||
|
||||
- Points to a single SalesOrder object
|
||||
- Multiple SalesOrderAllocation objects point to a particular SalesOrderShipment
|
||||
@ -1297,14 +1216,12 @@ class SalesOrderShipment(models.Model):
|
||||
|
||||
@transaction.atomic
|
||||
def complete_shipment(self, user, **kwargs):
|
||||
"""
|
||||
Complete this particular shipment:
|
||||
"""Complete this particular shipment:
|
||||
|
||||
1. Update any stock items associated with this shipment
|
||||
2. Update the "shipped" quantity of all associated line items
|
||||
3. Set the "shipment_date" to now
|
||||
"""
|
||||
|
||||
# Check if the shipment can be completed (throw error if not)
|
||||
self.check_can_complete()
|
||||
|
||||
@ -1343,8 +1260,8 @@ class SalesOrderShipment(models.Model):
|
||||
|
||||
|
||||
class SalesOrderExtraLine(OrderExtraLine):
|
||||
"""
|
||||
Model for a single ExtraLine in a SalesOrder
|
||||
"""Model for a single ExtraLine in a SalesOrder
|
||||
|
||||
Attributes:
|
||||
order: Link to the SalesOrder that this line belongs to
|
||||
title: title of line
|
||||
@ -1358,8 +1275,7 @@ class SalesOrderExtraLine(OrderExtraLine):
|
||||
|
||||
|
||||
class SalesOrderAllocation(models.Model):
|
||||
"""
|
||||
This model is used to 'allocate' stock items to a SalesOrder.
|
||||
"""This model is used to 'allocate' stock items to a SalesOrder.
|
||||
Items that are "allocated" to a SalesOrder are not yet "attached" to the order,
|
||||
but they will be once the order is fulfilled.
|
||||
|
||||
@ -1368,7 +1284,6 @@ class SalesOrderAllocation(models.Model):
|
||||
shipment: SalesOrderShipment reference
|
||||
item: StockItem reference
|
||||
quantity: Quantity to take from the StockItem
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@ -1376,8 +1291,7 @@ class SalesOrderAllocation(models.Model):
|
||||
return reverse('api-so-allocation-list')
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validate the SalesOrderAllocation object:
|
||||
"""Validate the SalesOrderAllocation object:
|
||||
|
||||
- Cannot allocate stock to a line item without a part reference
|
||||
- The referenced part must match the part associated with the line item
|
||||
@ -1385,7 +1299,6 @@ class SalesOrderAllocation(models.Model):
|
||||
- Allocation quantity must be "1" if the StockItem is serialized
|
||||
- Allocation quantity cannot be zero
|
||||
"""
|
||||
|
||||
super().clean()
|
||||
|
||||
errors = {}
|
||||
@ -1468,13 +1381,11 @@ class SalesOrderAllocation(models.Model):
|
||||
return self.item.purchase_order
|
||||
|
||||
def complete_allocation(self, user):
|
||||
"""
|
||||
Complete this allocation (called when the parent SalesOrder is marked as "shipped"):
|
||||
"""Complete this allocation (called when the parent SalesOrder is marked as "shipped"):
|
||||
|
||||
- Determine if the referenced StockItem needs to be "split" (if allocated quantity != stock quantity)
|
||||
- Mark the StockItem as belonging to the Customer (this will remove it from stock)
|
||||
"""
|
||||
|
||||
order = self.line.order
|
||||
|
||||
item = self.item.allocateToCustomer(
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON serializers for the Order API
|
||||
"""
|
||||
"""JSON serializers for the Order API"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
@ -33,9 +31,8 @@ from users.serializers import OwnerSerializer
|
||||
|
||||
|
||||
class AbstractOrderSerializer(serializers.Serializer):
|
||||
"""
|
||||
Abstract field definitions for OrderSerializers
|
||||
"""
|
||||
"""Abstract field definitions for OrderSerializers"""
|
||||
|
||||
total_price = InvenTreeMoneySerializer(
|
||||
source='get_total_price',
|
||||
allow_null=True,
|
||||
@ -46,7 +43,8 @@ class AbstractOrderSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class AbstractExtraLineSerializer(serializers.Serializer):
|
||||
""" Abstract Serializer for a ExtraLine object """
|
||||
"""Abstract Serializer for a ExtraLine object"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
order_detail = kwargs.pop('order_detail', False)
|
||||
@ -71,9 +69,7 @@ class AbstractExtraLineSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class AbstractExtraLineMeta:
|
||||
"""
|
||||
Abstract Meta for ExtraLine
|
||||
"""
|
||||
"""Abstract Meta for ExtraLine"""
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
@ -90,7 +86,7 @@ class AbstractExtraLineMeta:
|
||||
|
||||
|
||||
class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
""" Serializer for a PurchaseOrder object """
|
||||
"""Serializer for a PurchaseOrder object"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -103,13 +99,11 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add extra information to the queryset
|
||||
"""Add extra information to the queryset
|
||||
|
||||
- Number of lines in the PurchaseOrder
|
||||
- Overdue status of the PurchaseOrder
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
line_items=SubqueryCount('lines')
|
||||
)
|
||||
@ -172,18 +166,13 @@ class PurchaseOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializ
|
||||
|
||||
|
||||
class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for cancelling a PurchaseOrder
|
||||
"""
|
||||
"""Serializer for cancelling a PurchaseOrder"""
|
||||
|
||||
class Meta:
|
||||
fields = [],
|
||||
|
||||
def get_context_data(self):
|
||||
"""
|
||||
Return custom context information about the order
|
||||
"""
|
||||
|
||||
"""Return custom context information about the order"""
|
||||
self.order = self.context['order']
|
||||
|
||||
return {
|
||||
@ -201,18 +190,13 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for completing a purchase order
|
||||
"""
|
||||
"""Serializer for completing a purchase order"""
|
||||
|
||||
class Meta:
|
||||
fields = []
|
||||
|
||||
def get_context_data(self):
|
||||
"""
|
||||
Custom context information for this serializer
|
||||
"""
|
||||
|
||||
"""Custom context information for this serializer"""
|
||||
order = self.context['order']
|
||||
|
||||
return {
|
||||
@ -226,7 +210,7 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PurchaseOrderIssueSerializer(serializers.Serializer):
|
||||
""" Serializer for issuing (sending) a purchase order """
|
||||
"""Serializer for issuing (sending) a purchase order"""
|
||||
|
||||
class Meta:
|
||||
fields = []
|
||||
@ -241,13 +225,11 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add some extra annotations to this queryset:
|
||||
"""Add some extra annotations to this queryset:
|
||||
|
||||
- Total price = purchase_price * quantity
|
||||
- "Overdue" status (boolean field)
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
total_price=ExpressionWrapper(
|
||||
F('purchase_price') * F('quantity'),
|
||||
@ -374,7 +356,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
||||
""" Serializer for a PurchaseOrderExtraLine object """
|
||||
"""Serializer for a PurchaseOrderExtraLine object"""
|
||||
|
||||
order_detail = PurchaseOrderSerializer(source='order', many=False, read_only=True)
|
||||
|
||||
@ -383,9 +365,7 @@ class PurchaseOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeMod
|
||||
|
||||
|
||||
class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer for receiving a single purchase order line item against a purchase order
|
||||
"""
|
||||
"""A serializer for receiving a single purchase order line item against a purchase order"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
@ -468,10 +448,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_barcode(self, barcode):
|
||||
"""
|
||||
Cannot check in a LineItem with a barcode that is already assigned
|
||||
"""
|
||||
|
||||
"""Cannot check in a LineItem with a barcode that is already assigned"""
|
||||
# Ignore empty barcode values
|
||||
if not barcode or barcode.strip() == '':
|
||||
return None
|
||||
@ -513,9 +490,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for receiving items against a purchase order
|
||||
"""
|
||||
"""Serializer for receiving items against a purchase order"""
|
||||
|
||||
items = PurchaseOrderLineItemReceiveSerializer(many=True)
|
||||
|
||||
@ -571,9 +546,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Perform the actual database transaction to receive purchase order items
|
||||
"""
|
||||
"""Perform the actual database transaction to receive purchase order items"""
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
@ -613,9 +586,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializers for the PurchaseOrderAttachment model
|
||||
"""
|
||||
"""Serializers for the PurchaseOrderAttachment model"""
|
||||
|
||||
class Meta:
|
||||
model = order.models.PurchaseOrderAttachment
|
||||
@ -636,9 +607,7 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
|
||||
|
||||
class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrder object
|
||||
"""
|
||||
"""Serializers for the SalesOrder object"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -651,13 +620,11 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add extra information to the queryset
|
||||
"""Add extra information to the queryset
|
||||
|
||||
- Number of line items in the SalesOrder
|
||||
- Overdue status of the SalesOrder
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
line_items=SubqueryCount('lines')
|
||||
)
|
||||
@ -715,8 +682,8 @@ class SalesOrderSerializer(AbstractOrderSerializer, ReferenceIndexingSerializerM
|
||||
|
||||
|
||||
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for the SalesOrderAllocation model.
|
||||
"""Serializer for the SalesOrderAllocation model.
|
||||
|
||||
This includes some fields from the related model objects.
|
||||
"""
|
||||
|
||||
@ -783,16 +750,14 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for a SalesOrderLineItem object """
|
||||
"""Serializer for a SalesOrderLineItem object"""
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add some extra annotations to this queryset:
|
||||
"""Add some extra annotations to this queryset:
|
||||
|
||||
- "Overdue" status (boolean field)
|
||||
"""
|
||||
|
||||
queryset = queryset.annotate(
|
||||
overdue=Case(
|
||||
When(
|
||||
@ -866,9 +831,7 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for the SalesOrderShipment class
|
||||
"""
|
||||
"""Serializer for the SalesOrderShipment class"""
|
||||
|
||||
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
||||
|
||||
@ -893,9 +856,7 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for completing (shipping) a SalesOrderShipment
|
||||
"""
|
||||
"""Serializer for completing (shipping) a SalesOrderShipment"""
|
||||
|
||||
class Meta:
|
||||
model = order.models.SalesOrderShipment
|
||||
@ -945,9 +906,7 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer for allocating a single stock-item against a SalesOrder shipment
|
||||
"""
|
||||
"""A serializer for allocating a single stock-item against a SalesOrder shipment"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
@ -1019,9 +978,7 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for manually marking a sales order as complete
|
||||
"""
|
||||
"""DRF serializer for manually marking a sales order as complete"""
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
@ -1044,8 +1001,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderCancelSerializer(serializers.Serializer):
|
||||
""" Serializer for marking a SalesOrder as cancelled
|
||||
"""
|
||||
"""Serializer for marking a SalesOrder as cancelled"""
|
||||
|
||||
def get_context_data(self):
|
||||
|
||||
@ -1063,9 +1019,7 @@ class SalesOrderCancelSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for allocation of serial numbers against a sales order / shipment
|
||||
"""
|
||||
"""DRF serializer for allocation of serial numbers against a sales order / shipment"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
@ -1084,10 +1038,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_line_item(self, line_item):
|
||||
"""
|
||||
Ensure that the line_item is valid
|
||||
"""
|
||||
|
||||
"""Ensure that the line_item is valid"""
|
||||
order = self.context['order']
|
||||
|
||||
# Ensure that the line item points to the correct order
|
||||
@ -1119,13 +1070,11 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_shipment(self, shipment):
|
||||
"""
|
||||
Validate the shipment:
|
||||
"""Validate the shipment:
|
||||
|
||||
- Must point to the same order
|
||||
- Must not be shipped
|
||||
"""
|
||||
|
||||
order = self.context['order']
|
||||
|
||||
if shipment.shipment_date is not None:
|
||||
@ -1137,14 +1086,12 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
return shipment
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Validation for the serializer:
|
||||
"""Validation for the serializer:
|
||||
|
||||
- Ensure the serial_numbers and quantity fields match
|
||||
- Check that all serial numbers exist
|
||||
- Check that the serial numbers are not yet allocated
|
||||
"""
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
line_item = data['line_item']
|
||||
@ -1226,9 +1173,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for allocation of stock items against a sales order / shipment
|
||||
"""
|
||||
"""DRF serializer for allocation of stock items against a sales order / shipment"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
@ -1247,10 +1192,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_shipment(self, shipment):
|
||||
"""
|
||||
Run validation against the provided shipment instance
|
||||
"""
|
||||
|
||||
"""Run validation against the provided shipment instance"""
|
||||
order = self.context['order']
|
||||
|
||||
if shipment.shipment_date is not None:
|
||||
@ -1262,10 +1204,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
return shipment
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Serializer validation
|
||||
"""
|
||||
|
||||
"""Serializer validation"""
|
||||
data = super().validate(data)
|
||||
|
||||
# Extract SalesOrder from serializer context
|
||||
@ -1279,10 +1218,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Perform the allocation of items against this order
|
||||
"""
|
||||
|
||||
"""Perform the allocation of items against this order"""
|
||||
data = self.validated_data
|
||||
|
||||
items = data['items']
|
||||
@ -1304,7 +1240,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer):
|
||||
""" Serializer for a SalesOrderExtraLine object """
|
||||
"""Serializer for a SalesOrderExtraLine object"""
|
||||
|
||||
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
||||
|
||||
@ -1313,9 +1249,7 @@ class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelS
|
||||
|
||||
|
||||
class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializers for the SalesOrderAttachment model
|
||||
"""
|
||||
"""Serializers for the SalesOrderAttachment model"""
|
||||
|
||||
class Meta:
|
||||
model = order.models.SalesOrderAttachment
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Tests for the Order API
|
||||
"""
|
||||
"""Tests for the Order API"""
|
||||
|
||||
import io
|
||||
from datetime import datetime, timedelta
|
||||
@ -39,10 +37,7 @@ class OrderTest(InvenTreeAPITestCase):
|
||||
super().setUp()
|
||||
|
||||
def filter(self, filters, count):
|
||||
"""
|
||||
Test API filters
|
||||
"""
|
||||
|
||||
"""Test API filters"""
|
||||
response = self.get(
|
||||
self.LIST_URL,
|
||||
filters
|
||||
@ -55,9 +50,7 @@ class OrderTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class PurchaseOrderTest(OrderTest):
|
||||
"""
|
||||
Tests for the PurchaseOrder API
|
||||
"""
|
||||
"""Tests for the PurchaseOrder API"""
|
||||
|
||||
LIST_URL = reverse('api-po-list')
|
||||
|
||||
@ -79,10 +72,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.filter({'status': 40}, 1)
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Test "overdue" status
|
||||
"""
|
||||
|
||||
"""Test "overdue" status"""
|
||||
self.filter({'overdue': True}, 0)
|
||||
self.filter({'overdue': False}, 7)
|
||||
|
||||
@ -133,10 +123,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_po_operations(self):
|
||||
"""
|
||||
Test that we can create / edit and delete a PurchaseOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create / edit and delete a PurchaseOrder via the API"""
|
||||
n = models.PurchaseOrder.objects.count()
|
||||
|
||||
url = reverse('api-po-list')
|
||||
@ -223,10 +210,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
response = self.get(url, expected_code=404)
|
||||
|
||||
def test_po_create(self):
|
||||
"""
|
||||
Test that we can create a new PurchaseOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create a new PurchaseOrder via the API"""
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
self.post(
|
||||
@ -240,10 +224,7 @@ class PurchaseOrderTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_po_cancel(self):
|
||||
"""
|
||||
Test the PurchaseOrderCancel API endpoint
|
||||
"""
|
||||
|
||||
"""Test the PurchaseOrderCancel API endpoint"""
|
||||
po = models.PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(po.status, PurchaseOrderStatus.PENDING)
|
||||
@ -270,7 +251,6 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
def test_po_complete(self):
|
||||
""" Test the PurchaseOrderComplete API endpoint """
|
||||
|
||||
po = models.PurchaseOrder.objects.get(pk=3)
|
||||
|
||||
url = reverse('api-po-complete', kwargs={'pk': po.pk})
|
||||
@ -290,7 +270,6 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
def test_po_issue(self):
|
||||
""" Test the PurchaseOrderIssue API endpoint """
|
||||
|
||||
po = models.PurchaseOrder.objects.get(pk=2)
|
||||
|
||||
url = reverse('api-po-issue', kwargs={'pk': po.pk})
|
||||
@ -395,9 +374,7 @@ class PurchaseOrderDownloadTest(OrderTest):
|
||||
|
||||
|
||||
class PurchaseOrderReceiveTest(OrderTest):
|
||||
"""
|
||||
Unit tests for receiving items against a PurchaseOrder
|
||||
"""
|
||||
"""Unit tests for receiving items against a PurchaseOrder"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@ -415,10 +392,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
order.save()
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
Test without any POST data
|
||||
"""
|
||||
|
||||
"""Test without any POST data"""
|
||||
data = self.post(self.url, {}, expected_code=400).data
|
||||
|
||||
self.assertIn('This field is required', str(data['items']))
|
||||
@ -428,10 +402,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_no_items(self):
|
||||
"""
|
||||
Test with an empty list of items
|
||||
"""
|
||||
|
||||
"""Test with an empty list of items"""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -447,10 +418,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_invalid_items(self):
|
||||
"""
|
||||
Test than errors are returned as expected for invalid data
|
||||
"""
|
||||
|
||||
"""Test than errors are returned as expected for invalid data"""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -473,10 +441,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_invalid_status(self):
|
||||
"""
|
||||
Test with an invalid StockStatus value
|
||||
"""
|
||||
|
||||
"""Test with an invalid StockStatus value"""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -498,10 +463,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_mismatched_items(self):
|
||||
"""
|
||||
Test for supplier parts which *do* exist but do not match the order supplier
|
||||
"""
|
||||
|
||||
"""Test for supplier parts which *do* exist but do not match the order supplier"""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -523,10 +485,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_null_barcode(self):
|
||||
"""
|
||||
Test than a "null" barcode field can be provided
|
||||
"""
|
||||
|
||||
"""Test than a "null" barcode field can be provided"""
|
||||
# Set stock item barcode
|
||||
item = StockItem.objects.get(pk=1)
|
||||
item.save()
|
||||
@ -548,13 +507,11 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_invalid_barcodes(self):
|
||||
"""
|
||||
Tests for checking in items with invalid barcodes:
|
||||
"""Tests for checking in items with invalid barcodes:
|
||||
|
||||
- Cannot check in "duplicate" barcodes
|
||||
- Barcodes cannot match UID field for existing StockItem
|
||||
"""
|
||||
|
||||
# Set stock item barcode
|
||||
item = StockItem.objects.get(pk=1)
|
||||
item.uid = 'MY-BARCODE-HASH'
|
||||
@ -603,10 +560,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(self.n, StockItem.objects.count())
|
||||
|
||||
def test_valid(self):
|
||||
"""
|
||||
Test receipt of valid data
|
||||
"""
|
||||
|
||||
"""Test receipt of valid data"""
|
||||
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||
|
||||
@ -683,10 +637,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
|
||||
|
||||
def test_batch_code(self):
|
||||
"""
|
||||
Test that we can supply a 'batch code' when receiving items
|
||||
"""
|
||||
|
||||
"""Test that we can supply a 'batch code' when receiving items"""
|
||||
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||
|
||||
@ -727,10 +678,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
self.assertEqual(item_2.batch, 'xyz-789')
|
||||
|
||||
def test_serial_numbers(self):
|
||||
"""
|
||||
Test that we can supply a 'serial number' when receiving items
|
||||
"""
|
||||
|
||||
"""Test that we can supply a 'serial number' when receiving items"""
|
||||
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||
|
||||
@ -786,9 +734,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
||||
|
||||
|
||||
class SalesOrderTest(OrderTest):
|
||||
"""
|
||||
Tests for the SalesOrder API
|
||||
"""
|
||||
"""Tests for the SalesOrder API"""
|
||||
|
||||
LIST_URL = reverse('api-so-list')
|
||||
|
||||
@ -843,10 +789,7 @@ class SalesOrderTest(OrderTest):
|
||||
self.get(url)
|
||||
|
||||
def test_so_operations(self):
|
||||
"""
|
||||
Test that we can create / edit and delete a SalesOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create / edit and delete a SalesOrder via the API"""
|
||||
n = models.SalesOrder.objects.count()
|
||||
|
||||
url = reverse('api-so-list')
|
||||
@ -926,10 +869,7 @@ class SalesOrderTest(OrderTest):
|
||||
response = self.get(url, expected_code=404)
|
||||
|
||||
def test_so_create(self):
|
||||
"""
|
||||
Test that we can create a new SalesOrder via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create a new SalesOrder via the API"""
|
||||
self.assignRole('sales_order.add')
|
||||
|
||||
self.post(
|
||||
@ -980,9 +920,7 @@ class SalesOrderTest(OrderTest):
|
||||
|
||||
|
||||
class SalesOrderLineItemTest(OrderTest):
|
||||
"""
|
||||
Tests for the SalesOrderLineItem API
|
||||
"""
|
||||
"""Tests for the SalesOrderLineItem API"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@ -1064,7 +1002,6 @@ class SalesOrderDownloadTest(OrderTest):
|
||||
|
||||
def test_download_fail(self):
|
||||
"""Test that downloading without the 'export' option fails"""
|
||||
|
||||
url = reverse('api-so-list')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
@ -1151,9 +1088,7 @@ class SalesOrderDownloadTest(OrderTest):
|
||||
|
||||
|
||||
class SalesOrderAllocateTest(OrderTest):
|
||||
"""
|
||||
Unit tests for allocating stock items against a SalesOrder
|
||||
"""
|
||||
"""Unit tests for allocating stock items against a SalesOrder"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@ -1188,10 +1123,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
)
|
||||
|
||||
def test_invalid(self):
|
||||
"""
|
||||
Test POST with invalid data
|
||||
"""
|
||||
|
||||
"""Test POST with invalid data"""
|
||||
# No data
|
||||
response = self.post(self.url, {}, expected_code=400)
|
||||
|
||||
@ -1244,11 +1176,7 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
self.assertIn('Shipment is not associated with this order', str(response.data['shipment']))
|
||||
|
||||
def test_allocate(self):
|
||||
"""
|
||||
Test the the allocation endpoint acts as expected,
|
||||
when provided with valid data!
|
||||
"""
|
||||
|
||||
"""Test the the allocation endpoint acts as expected, when provided with valid data!"""
|
||||
# First, check that there are no line items allocated against this SalesOrder
|
||||
self.assertEqual(self.order.stock_allocations.count(), 0)
|
||||
|
||||
@ -1279,7 +1207,6 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
|
||||
def test_shipment_complete(self):
|
||||
"""Test that we can complete a shipment via the API"""
|
||||
|
||||
url = reverse('api-so-shipment-ship', kwargs={'pk': self.shipment.pk})
|
||||
|
||||
self.assertFalse(self.shipment.is_complete())
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Unit tests for the 'order' model data migrations
|
||||
"""
|
||||
"""Unit tests for the 'order' model data migrations"""
|
||||
|
||||
from django_test_migrations.contrib.unittest_case import MigratorTestCase
|
||||
|
||||
@ -8,18 +6,13 @@ from InvenTree.status_codes import SalesOrderStatus
|
||||
|
||||
|
||||
class TestRefIntMigrations(MigratorTestCase):
|
||||
"""
|
||||
Test entire schema migration
|
||||
"""
|
||||
"""Test entire schema migration"""
|
||||
|
||||
migrate_from = ('order', '0040_salesorder_target_date')
|
||||
migrate_to = ('order', '0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create initial data set
|
||||
"""
|
||||
|
||||
"""Create initial data set"""
|
||||
# Create a purchase order from a supplier
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
|
||||
@ -57,10 +50,7 @@ class TestRefIntMigrations(MigratorTestCase):
|
||||
print(sales_order.reference_int)
|
||||
|
||||
def test_ref_field(self):
|
||||
"""
|
||||
Test that the 'reference_int' field has been created and is filled out correctly
|
||||
"""
|
||||
|
||||
"""Test that the 'reference_int' field has been created and is filled out correctly"""
|
||||
PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
|
||||
SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
|
||||
|
||||
@ -75,18 +65,13 @@ class TestRefIntMigrations(MigratorTestCase):
|
||||
|
||||
|
||||
class TestShipmentMigration(MigratorTestCase):
|
||||
"""
|
||||
Test data migration for the "SalesOrderShipment" model
|
||||
"""
|
||||
"""Test data migration for the "SalesOrderShipment" model"""
|
||||
|
||||
migrate_from = ('order', '0051_auto_20211014_0623')
|
||||
migrate_to = ('order', '0055_auto_20211025_0645')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create an initial SalesOrder
|
||||
"""
|
||||
|
||||
"""Create an initial SalesOrder"""
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
|
||||
customer = Company.objects.create(
|
||||
@ -112,10 +97,7 @@ class TestShipmentMigration(MigratorTestCase):
|
||||
self.old_state.apps.get_model('order', 'salesordershipment')
|
||||
|
||||
def test_shipment_creation(self):
|
||||
"""
|
||||
Check that a SalesOrderShipment has been created
|
||||
"""
|
||||
|
||||
"""Check that a SalesOrderShipment has been created"""
|
||||
SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
|
||||
Shipment = self.new_state.apps.get_model('order', 'salesordershipment')
|
||||
|
||||
@ -125,18 +107,13 @@ class TestShipmentMigration(MigratorTestCase):
|
||||
|
||||
|
||||
class TestAdditionalLineMigration(MigratorTestCase):
|
||||
"""
|
||||
Test entire schema migration
|
||||
"""
|
||||
"""Test entire schema migration"""
|
||||
|
||||
migrate_from = ('order', '0063_alter_purchaseorderlineitem_unique_together')
|
||||
migrate_to = ('order', '0064_purchaseorderextraline_salesorderextraline')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create initial data set
|
||||
"""
|
||||
|
||||
"""Create initial data set"""
|
||||
# Create a purchase order from a supplier
|
||||
Company = self.old_state.apps.get_model('company', 'company')
|
||||
PurchaseOrder = self.old_state.apps.get_model('order', 'purchaseorder')
|
||||
@ -199,10 +176,7 @@ class TestAdditionalLineMigration(MigratorTestCase):
|
||||
# )
|
||||
|
||||
def test_po_migration(self):
|
||||
"""
|
||||
Test that the the PO lines where converted correctly
|
||||
"""
|
||||
|
||||
"""Test that the the PO lines where converted correctly"""
|
||||
PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
|
||||
for ii in range(10):
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -15,10 +13,7 @@ from stock.models import StockItem
|
||||
|
||||
|
||||
class SalesOrderTest(TestCase):
|
||||
"""
|
||||
Run tests to ensure that the SalesOrder model is working correctly.
|
||||
|
||||
"""
|
||||
"""Run tests to ensure that the SalesOrder model is working correctly."""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@ -49,9 +44,7 @@ class SalesOrderTest(TestCase):
|
||||
self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part)
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Tests for overdue functionality
|
||||
"""
|
||||
"""Tests for overdue functionality"""
|
||||
|
||||
today = datetime.now().date()
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
""" Unit tests for Order views (see views.py) """
|
||||
"""Unit tests for Order views (see views.py)"""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -37,7 +37,7 @@ class OrderListTest(OrderViewTestCase):
|
||||
|
||||
|
||||
class PurchaseOrderTests(OrderViewTestCase):
|
||||
""" Tests for PurchaseOrder views """
|
||||
"""Tests for PurchaseOrder views"""
|
||||
|
||||
def test_detail_view(self):
|
||||
""" Retrieve PO detail view """
|
||||
@ -47,8 +47,7 @@ class PurchaseOrderTests(OrderViewTestCase):
|
||||
self.assertIn('PurchaseOrderStatus', keys)
|
||||
|
||||
def test_po_export(self):
|
||||
""" Export PurchaseOrder """
|
||||
|
||||
"""Export PurchaseOrder"""
|
||||
response = self.client.get(reverse('po-export', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Response should be streaming-content (file download)
|
||||
|
@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import django.core.exceptions as django_exceptions
|
||||
@ -14,9 +12,7 @@ from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
|
||||
|
||||
class OrderTest(TestCase):
|
||||
"""
|
||||
Tests to ensure that the order models are functioning correctly.
|
||||
"""
|
||||
"""Tests to ensure that the order models are functioning correctly."""
|
||||
|
||||
fixtures = [
|
||||
'company',
|
||||
@ -30,8 +26,7 @@ class OrderTest(TestCase):
|
||||
]
|
||||
|
||||
def test_basics(self):
|
||||
""" Basic tests e.g. repr functions etc """
|
||||
|
||||
"""Basic tests e.g. repr functions etc"""
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
|
||||
@ -43,10 +38,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Test overdue status functionality
|
||||
"""
|
||||
|
||||
"""Test overdue status functionality"""
|
||||
today = datetime.now().date()
|
||||
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
@ -61,8 +53,7 @@ class OrderTest(TestCase):
|
||||
self.assertFalse(order.is_overdue)
|
||||
|
||||
def test_on_order(self):
|
||||
""" There should be 3 separate items on order for the M2x4 LPHS part """
|
||||
|
||||
"""There should be 3 separate items on order for the M2x4 LPHS part"""
|
||||
part = Part.objects.get(name='M2x4 LPHS')
|
||||
|
||||
open_orders = []
|
||||
@ -76,8 +67,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(part.on_order, 1400)
|
||||
|
||||
def test_add_items(self):
|
||||
""" Test functions for adding line items to an order """
|
||||
|
||||
"""Test functions for adding line items to an order"""
|
||||
order = PurchaseOrder.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||
@ -113,8 +103,7 @@ class OrderTest(TestCase):
|
||||
order.add_line_item(sku, 99)
|
||||
|
||||
def test_pricing(self):
|
||||
""" Test functions for adding line items to an order including price-breaks """
|
||||
|
||||
"""Test functions for adding line items to an order including price-breaks"""
|
||||
order = PurchaseOrder.objects.get(pk=7)
|
||||
|
||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||
@ -146,8 +135,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(order.lines.first().purchase_price.amount, 1.25)
|
||||
|
||||
def test_receive(self):
|
||||
""" Test order receiving functions """
|
||||
|
||||
"""Test order receiving functions"""
|
||||
part = Part.objects.get(name='M2x4 LPHS')
|
||||
|
||||
# Receive some items
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""
|
||||
URL lookup for the Order app. Provides URL endpoints for:
|
||||
"""URL lookup for the Order app. Provides URL endpoints for:
|
||||
|
||||
- List view of Purchase Orders
|
||||
- Detail view of Purchase Orders
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django views for interacting with Order app
|
||||
"""
|
||||
"""Django views for interacting with Order app"""
|
||||
|
||||
import logging
|
||||
from decimal import Decimal, InvalidOperation
|
||||
@ -33,7 +31,7 @@ logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
""" List view for all purchase orders """
|
||||
"""List view for all purchase orders"""
|
||||
|
||||
model = PurchaseOrder
|
||||
template_name = 'order/purchase_orders.html'
|
||||
@ -61,7 +59,7 @@ class SalesOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
|
||||
|
||||
class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
""" Detail view for a PurchaseOrder object """
|
||||
"""Detail view for a PurchaseOrder object"""
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = PurchaseOrder.objects.all().prefetch_related('lines')
|
||||
@ -74,7 +72,7 @@ class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
|
||||
|
||||
|
||||
class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
""" Detail view for a SalesOrder object """
|
||||
"""Detail view for a SalesOrder object"""
|
||||
|
||||
context_object_name = 'order'
|
||||
queryset = SalesOrder.objects.all().prefetch_related('lines__allocations__item__purchase_order')
|
||||
@ -82,7 +80,7 @@ class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView)
|
||||
|
||||
|
||||
class PurchaseOrderUpload(FileManagementFormView):
|
||||
''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) '''
|
||||
"""PurchaseOrder: Upload file, match to fields and parts (using multi-Step form)"""
|
||||
|
||||
class OrderFileManager(FileManager):
|
||||
REQUIRED_HEADERS = [
|
||||
@ -126,12 +124,12 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
file_manager_class = OrderFileManager
|
||||
|
||||
def get_order(self):
|
||||
""" Get order or return 404 """
|
||||
"""Get order or return 404"""
|
||||
|
||||
return get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
||||
|
||||
def get_context_data(self, form, **kwargs):
|
||||
""" Handle context data for order """
|
||||
"""Handle context data for order"""
|
||||
|
||||
context = super().get_context_data(form=form, **kwargs)
|
||||
|
||||
@ -142,11 +140,11 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
return context
|
||||
|
||||
def get_field_selection(self):
|
||||
""" Once data columns have been selected, attempt to pre-select the proper data from the database.
|
||||
"""Once data columns have been selected, attempt to pre-select the proper data from the database.
|
||||
|
||||
This function is called once the field selection has been validated.
|
||||
The pre-fill data are then passed through to the SupplierPart selection form.
|
||||
"""
|
||||
|
||||
order = self.get_order()
|
||||
|
||||
self.allowed_items = SupplierPart.objects.filter(supplier=order.supplier).prefetch_related('manufacturer_part')
|
||||
@ -231,8 +229,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
row['notes'] = notes
|
||||
|
||||
def done(self, form_list, **kwargs):
|
||||
""" Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """
|
||||
|
||||
"""Once all the data is in, process it to add PurchaseOrderLineItem instances to the order"""
|
||||
order = self.get_order()
|
||||
items = self.get_clean_items()
|
||||
|
||||
@ -263,8 +260,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
||||
|
||||
|
||||
class SalesOrderExport(AjaxView):
|
||||
"""
|
||||
Export a sales order
|
||||
"""Export a sales order
|
||||
|
||||
- File format can optionally be passed as a query parameter e.g. ?format=CSV
|
||||
- Default file format is CSV
|
||||
@ -290,7 +286,7 @@ class SalesOrderExport(AjaxView):
|
||||
|
||||
|
||||
class PurchaseOrderExport(AjaxView):
|
||||
""" File download for a purchase order
|
||||
"""File download for a purchase order
|
||||
|
||||
- File format can be optionally passed as a query param e.g. ?format=CSV
|
||||
- Default file format is CSV
|
||||
@ -321,7 +317,7 @@ class PurchaseOrderExport(AjaxView):
|
||||
|
||||
|
||||
class LineItemPricing(PartPricing):
|
||||
""" View for inspecting part pricing information """
|
||||
"""View for inspecting part pricing information"""
|
||||
|
||||
class EnhancedForm(PartPricing.form_class):
|
||||
pk = IntegerField(widget=HiddenInput())
|
||||
@ -365,7 +361,7 @@ class LineItemPricing(PartPricing):
|
||||
return None
|
||||
|
||||
def get_quantity(self):
|
||||
""" Return set quantity in decimal format """
|
||||
"""Return set quantity in decimal format"""
|
||||
qty = Decimal(self.request.GET.get('quantity', 1))
|
||||
if qty == 1:
|
||||
return Decimal(self.request.POST.get('quantity', 1))
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Provides a JSON API for the Part app
|
||||
"""
|
||||
"""Provides a JSON API for the Part app"""
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
@ -41,7 +39,7 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
|
||||
|
||||
|
||||
class CategoryList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PartCategory objects.
|
||||
"""API endpoint for accessing a list of PartCategory objects.
|
||||
|
||||
- GET: Return a list of PartCategory objects
|
||||
- POST: Create a new PartCategory object
|
||||
@ -63,11 +61,10 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
return ctx
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering:
|
||||
"""Custom filtering:
|
||||
|
||||
- Allow filtering by "null" parent to retrieve top-level part categories
|
||||
"""
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -158,9 +155,7 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a single PartCategory object
|
||||
"""
|
||||
"""API endpoint for detail view of a single PartCategory object"""
|
||||
|
||||
serializer_class = part_serializers.CategorySerializer
|
||||
queryset = PartCategory.objects.all()
|
||||
@ -199,7 +194,7 @@ class CategoryMetadata(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class CategoryParameterList(generics.ListAPIView):
|
||||
""" API endpoint for accessing a list of PartCategoryParameterTemplate objects.
|
||||
"""API endpoint for accessing a list of PartCategoryParameterTemplate objects.
|
||||
|
||||
- GET: Return a list of PartCategoryParameterTemplate objects
|
||||
"""
|
||||
@ -208,13 +203,12 @@ class CategoryParameterList(generics.ListAPIView):
|
||||
serializer_class = part_serializers.CategoryParameterTemplateSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Custom filtering:
|
||||
"""Custom filtering:
|
||||
|
||||
- Allow filtering by "null" parent to retrieve all categories parameter templates
|
||||
- Allow filtering by category
|
||||
- Allow traversing all parent categories
|
||||
"""
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
params = self.request.query_params
|
||||
@ -241,9 +235,7 @@ class CategoryParameterList(generics.ListAPIView):
|
||||
|
||||
|
||||
class CategoryTree(generics.ListAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of PartCategory objects ready for rendering a tree.
|
||||
"""
|
||||
"""API endpoint for accessing a list of PartCategory objects ready for rendering a tree."""
|
||||
|
||||
queryset = PartCategory.objects.all()
|
||||
serializer_class = part_serializers.CategoryTree
|
||||
@ -258,18 +250,14 @@ class CategoryTree(generics.ListAPIView):
|
||||
|
||||
|
||||
class PartSalePriceDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail endpoint for PartSellPriceBreak model
|
||||
"""
|
||||
"""Detail endpoint for PartSellPriceBreak model"""
|
||||
|
||||
queryset = PartSellPriceBreak.objects.all()
|
||||
serializer_class = part_serializers.PartSalePriceSerializer
|
||||
|
||||
|
||||
class PartSalePriceList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for list view of PartSalePriceBreak model
|
||||
"""
|
||||
"""API endpoint for list view of PartSalePriceBreak model"""
|
||||
|
||||
queryset = PartSellPriceBreak.objects.all()
|
||||
serializer_class = part_serializers.PartSalePriceSerializer
|
||||
@ -284,18 +272,14 @@ class PartSalePriceList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PartInternalPriceDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail endpoint for PartInternalPriceBreak model
|
||||
"""
|
||||
"""Detail endpoint for PartInternalPriceBreak model"""
|
||||
|
||||
queryset = PartInternalPriceBreak.objects.all()
|
||||
serializer_class = part_serializers.PartInternalPriceSerializer
|
||||
|
||||
|
||||
class PartInternalPriceList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for list view of PartInternalPriceBreak model
|
||||
"""
|
||||
"""API endpoint for list view of PartInternalPriceBreak model"""
|
||||
|
||||
queryset = PartInternalPriceBreak.objects.all()
|
||||
serializer_class = part_serializers.PartInternalPriceSerializer
|
||||
@ -311,9 +295,7 @@ class PartInternalPriceList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PartAttachment (file upload).
|
||||
"""
|
||||
"""API endpoint for listing (and creating) a PartAttachment (file upload)."""
|
||||
|
||||
queryset = PartAttachment.objects.all()
|
||||
serializer_class = part_serializers.PartAttachmentSerializer
|
||||
@ -328,38 +310,30 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
|
||||
|
||||
class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for PartAttachment model
|
||||
"""
|
||||
"""Detail endpoint for PartAttachment model"""
|
||||
|
||||
queryset = PartAttachment.objects.all()
|
||||
serializer_class = part_serializers.PartAttachmentSerializer
|
||||
|
||||
|
||||
class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail endpoint for PartTestTemplate model
|
||||
"""
|
||||
"""Detail endpoint for PartTestTemplate model"""
|
||||
|
||||
queryset = PartTestTemplate.objects.all()
|
||||
serializer_class = part_serializers.PartTestTemplateSerializer
|
||||
|
||||
|
||||
class PartTestTemplateList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PartTestTemplate.
|
||||
"""
|
||||
"""API endpoint for listing (and creating) a PartTestTemplate."""
|
||||
|
||||
queryset = PartTestTemplate.objects.all()
|
||||
serializer_class = part_serializers.PartTestTemplateSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Filter the test list queryset.
|
||||
"""Filter the test list queryset.
|
||||
|
||||
If filtering by 'part', we include results for any parts "above" the specified part.
|
||||
"""
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -390,9 +364,7 @@ class PartTestTemplateList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PartThumbs(generics.ListAPIView):
|
||||
"""
|
||||
API endpoint for retrieving information on available Part thumbnails
|
||||
"""
|
||||
"""API endpoint for retrieving information on available Part thumbnails"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = part_serializers.PartThumbSerializer
|
||||
@ -407,11 +379,10 @@ class PartThumbs(generics.ListAPIView):
|
||||
return queryset
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
Serialize the available Part images.
|
||||
"""Serialize the available Part images.
|
||||
|
||||
- Images may be used for multiple parts!
|
||||
"""
|
||||
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Return the most popular parts first
|
||||
@ -436,7 +407,7 @@ class PartThumbs(generics.ListAPIView):
|
||||
|
||||
|
||||
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
|
||||
""" API endpoint for updating Part thumbnails"""
|
||||
"""API endpoint for updating Part thumbnails"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = part_serializers.PartThumbSerializerUpdate
|
||||
@ -447,8 +418,7 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class PartScheduling(generics.RetrieveAPIView):
|
||||
"""
|
||||
API endpoint for delivering "scheduling" information about a given part via the API.
|
||||
"""API endpoint for delivering "scheduling" information about a given part via the API.
|
||||
|
||||
Returns a chronologically ordered list about future "scheduled" events,
|
||||
concerning stock levels for the part:
|
||||
@ -470,13 +440,12 @@ class PartScheduling(generics.RetrieveAPIView):
|
||||
schedule = []
|
||||
|
||||
def add_schedule_entry(date, quantity, title, label, url):
|
||||
"""
|
||||
Check if a scheduled entry should be added:
|
||||
"""Check if a scheduled entry should be added:
|
||||
|
||||
- date must be non-null
|
||||
- date cannot be in the "past"
|
||||
- quantity must not be zero
|
||||
"""
|
||||
|
||||
if date and date >= today and quantity != 0:
|
||||
schedule.append({
|
||||
'date': date,
|
||||
@ -583,9 +552,7 @@ class PartScheduling(generics.RetrieveAPIView):
|
||||
|
||||
|
||||
class PartMetadata(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
API endpoint for viewing / updating Part metadata
|
||||
"""
|
||||
"""API endpoint for viewing / updating Part metadata"""
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
return MetadataSerializer(Part, *args, **kwargs)
|
||||
@ -594,9 +561,7 @@ class PartMetadata(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class PartSerialNumberDetail(generics.RetrieveAPIView):
|
||||
"""
|
||||
API endpoint for returning extra serial number information about a particular part
|
||||
"""
|
||||
"""API endpoint for returning extra serial number information about a particular part"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
|
||||
@ -621,9 +586,7 @@ class PartSerialNumberDetail(generics.RetrieveAPIView):
|
||||
|
||||
|
||||
class PartCopyBOM(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for duplicating a BOM
|
||||
"""
|
||||
"""API endpoint for duplicating a BOM"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = part_serializers.PartCopyBOMSerializer
|
||||
@ -641,9 +604,7 @@ class PartCopyBOM(generics.CreateAPIView):
|
||||
|
||||
|
||||
class PartValidateBOM(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
API endpoint for 'validating' the BOM for a given Part
|
||||
"""
|
||||
"""API endpoint for 'validating' the BOM for a given Part"""
|
||||
|
||||
class BOMValidateSerializer(serializers.ModelSerializer):
|
||||
|
||||
@ -691,7 +652,7 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a single Part object """
|
||||
"""API endpoint for detail view of a single Part object"""
|
||||
|
||||
queryset = Part.objects.all()
|
||||
serializer_class = part_serializers.PartSerializer
|
||||
@ -738,12 +699,10 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data=message)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""
|
||||
Custom update functionality for Part instance.
|
||||
"""Custom update functionality for Part instance.
|
||||
|
||||
- If the 'starred' field is provided, update the 'starred' status against current user
|
||||
"""
|
||||
|
||||
if 'starred' in request.data:
|
||||
starred = str2bool(request.data.get('starred', False))
|
||||
|
||||
@ -755,8 +714,8 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
class PartFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for the PartList endpoint.
|
||||
"""Custom filters for the PartList endpoint.
|
||||
|
||||
Uses the django_filters extension framework
|
||||
"""
|
||||
|
||||
@ -791,9 +750,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
low_stock = rest_filters.BooleanFilter(label='Low stock', method='filter_low_stock')
|
||||
|
||||
def filter_low_stock(self, queryset, name, value):
|
||||
"""
|
||||
Filter by "low stock" status
|
||||
"""
|
||||
"""Filter by "low stock" status"""
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
@ -854,8 +811,7 @@ class PartFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class PartList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of Part objects
|
||||
"""API endpoint for accessing a list of Part objects
|
||||
|
||||
- GET: Return list of objects
|
||||
- POST: Create a new Part object
|
||||
@ -912,14 +868,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
Overide the 'list' method, as the PartCategory objects are
|
||||
very expensive to serialize!
|
||||
"""Overide the 'list' method, as the PartCategory objects are very expensive to serialize!
|
||||
|
||||
So we will serialize them first, and keep them in memory,
|
||||
so that they do not have to be serialized multiple times...
|
||||
So we will serialize them first, and keep them in memory, so that they do not have to be serialized multiple times...
|
||||
"""
|
||||
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
@ -980,12 +932,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
We wish to save the user who created this part!
|
||||
"""We wish to save the user who created this part!
|
||||
|
||||
Note: Implementation copied from DRF class CreateModelMixin
|
||||
"""
|
||||
|
||||
# TODO: Unit tests for this function!
|
||||
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
@ -1135,11 +1085,10 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Perform custom filtering of the queryset.
|
||||
"""Perform custom filtering of the queryset.
|
||||
|
||||
We overide the DRF filter_fields here because
|
||||
"""
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
@ -1391,9 +1340,7 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PartRelatedList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of PartRelated objects
|
||||
"""
|
||||
"""API endpoint for accessing a list of PartRelated objects"""
|
||||
|
||||
queryset = PartRelated.objects.all()
|
||||
serializer_class = part_serializers.PartRelationSerializer
|
||||
@ -1420,16 +1367,14 @@ class PartRelatedList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for accessing detail view of a PartRelated object
|
||||
"""
|
||||
"""API endpoint for accessing detail view of a PartRelated object"""
|
||||
|
||||
queryset = PartRelated.objects.all()
|
||||
serializer_class = part_serializers.PartRelationSerializer
|
||||
|
||||
|
||||
class PartParameterTemplateList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PartParameterTemplate objects.
|
||||
"""API endpoint for accessing a list of PartParameterTemplate objects.
|
||||
|
||||
- GET: Return list of PartParameterTemplate objects
|
||||
- POST: Create a new PartParameterTemplate object
|
||||
@ -1453,10 +1398,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering for the PartParameterTemplate API
|
||||
"""
|
||||
|
||||
"""Custom filtering for the PartParameterTemplate API"""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -1492,7 +1434,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PartParameterList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PartParameter objects
|
||||
"""API endpoint for accessing a list of PartParameter objects
|
||||
|
||||
- GET: Return list of PartParameter objects
|
||||
- POST: Create a new PartParameter object
|
||||
@ -1512,18 +1454,14 @@ class PartParameterList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a single PartParameter object
|
||||
"""
|
||||
"""API endpoint for detail view of a single PartParameter object"""
|
||||
|
||||
queryset = PartParameter.objects.all()
|
||||
serializer_class = part_serializers.PartParameterSerializer
|
||||
|
||||
|
||||
class BomFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for the BOM list
|
||||
"""
|
||||
"""Custom filters for the BOM list"""
|
||||
|
||||
# Boolean filters for BOM item
|
||||
optional = rest_filters.BooleanFilter(label='BOM line is optional')
|
||||
@ -1564,8 +1502,7 @@ class BomFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class BomList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of BomItem objects.
|
||||
"""API endpoint for accessing a list of BomItem objects.
|
||||
|
||||
- GET: Return list of BomItem objects
|
||||
- POST: Create a new BomItem object
|
||||
@ -1715,18 +1652,13 @@ class BomList(generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def include_pricing(self):
|
||||
"""
|
||||
Determine if pricing information should be included in the response
|
||||
"""
|
||||
"""Determine if pricing information should be included in the response"""
|
||||
pricing_default = InvenTreeSetting.get_setting('PART_SHOW_PRICE_IN_BOM')
|
||||
|
||||
return str2bool(self.request.query_params.get('include_pricing', pricing_default))
|
||||
|
||||
def annotate_pricing(self, queryset):
|
||||
"""
|
||||
Add part pricing information to the queryset
|
||||
"""
|
||||
|
||||
"""Add part pricing information to the queryset"""
|
||||
# Annotate with purchase prices
|
||||
queryset = queryset.annotate(
|
||||
purchase_price_min=Min('sub_part__stock_items__purchase_price'),
|
||||
@ -1741,8 +1673,7 @@ class BomList(generics.ListCreateAPIView):
|
||||
).values('pk', 'sub_part', 'purchase_price', 'purchase_price_currency')
|
||||
|
||||
def convert_price(price, currency, decimal_places=4):
|
||||
""" Convert price field, returns Money field """
|
||||
|
||||
"""Convert price field, returns Money field"""
|
||||
price_adjusted = None
|
||||
|
||||
# Get default currency from settings
|
||||
@ -1795,8 +1726,7 @@ class BomList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class BomImportUpload(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for uploading a complete Bill of Materials.
|
||||
"""API endpoint for uploading a complete Bill of Materials.
|
||||
|
||||
It is assumed that the BOM has been extracted from a file using the BomExtract endpoint.
|
||||
"""
|
||||
@ -1805,10 +1735,7 @@ class BomImportUpload(generics.CreateAPIView):
|
||||
serializer_class = part_serializers.BomImportUploadSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Custom create function to return the extracted data
|
||||
"""
|
||||
|
||||
"""Custom create function to return the extracted data"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
@ -1820,25 +1747,21 @@ class BomImportUpload(generics.CreateAPIView):
|
||||
|
||||
|
||||
class BomImportExtract(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for extracting BOM data from a BOM file.
|
||||
"""
|
||||
"""API endpoint for extracting BOM data from a BOM file."""
|
||||
|
||||
queryset = Part.objects.none()
|
||||
serializer_class = part_serializers.BomImportExtractSerializer
|
||||
|
||||
|
||||
class BomImportSubmit(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for submitting BOM data from a BOM file
|
||||
"""
|
||||
"""API endpoint for submitting BOM data from a BOM file"""
|
||||
|
||||
queryset = BomItem.objects.none()
|
||||
serializer_class = part_serializers.BomImportSubmitSerializer
|
||||
|
||||
|
||||
class BomDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a single BomItem object """
|
||||
"""API endpoint for detail view of a single BomItem object"""
|
||||
|
||||
queryset = BomItem.objects.all()
|
||||
serializer_class = part_serializers.BomItemSerializer
|
||||
@ -1854,7 +1777,7 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
class BomItemValidate(generics.UpdateAPIView):
|
||||
""" API endpoint for validating a BomItem """
|
||||
"""API endpoint for validating a BomItem"""
|
||||
|
||||
# Very simple serializers
|
||||
class BomItemValidationSerializer(serializers.Serializer):
|
||||
@ -1883,9 +1806,7 @@ class BomItemValidate(generics.UpdateAPIView):
|
||||
|
||||
|
||||
class BomItemSubstituteList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of BomItemSubstitute objects
|
||||
"""
|
||||
"""API endpoint for accessing a list of BomItemSubstitute objects"""
|
||||
|
||||
serializer_class = part_serializers.BomItemSubstituteSerializer
|
||||
queryset = BomItemSubstitute.objects.all()
|
||||
@ -1903,9 +1824,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class BomItemSubstituteDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a single BomItemSubstitute object
|
||||
"""
|
||||
"""API endpoint for detail view of a single BomItemSubstitute object"""
|
||||
|
||||
queryset = BomItemSubstitute.objects.all()
|
||||
serializer_class = part_serializers.BomItemSubstituteSerializer
|
||||
|
@ -12,21 +12,15 @@ class PartConfig(AppConfig):
|
||||
name = 'part'
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
This function is called whenever the Part app is loaded.
|
||||
"""
|
||||
|
||||
"""This function is called whenever the Part app is loaded."""
|
||||
if canAppAccessDatabase():
|
||||
self.update_trackable_status()
|
||||
|
||||
def update_trackable_status(self):
|
||||
"""
|
||||
Check for any instances where a trackable part is used in the BOM
|
||||
for a non-trackable part.
|
||||
"""Check for any instances where a trackable part is used in the BOM for a non-trackable part.
|
||||
|
||||
In such a case, force the top-level part to be trackable too.
|
||||
"""
|
||||
|
||||
from .models import BomItem
|
||||
|
||||
try:
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""
|
||||
Functionality for Bill of Material (BOM) management.
|
||||
"""Functionality for Bill of Material (BOM) management.
|
||||
Primarily BOM upload tools.
|
||||
"""
|
||||
|
||||
@ -15,14 +14,12 @@ from .models import BomItem
|
||||
|
||||
|
||||
def IsValidBOMFormat(fmt):
|
||||
""" Test if a file format specifier is in the valid list of BOM file formats """
|
||||
|
||||
"""Test if a file format specifier is in the valid list of BOM file formats"""
|
||||
return fmt.strip().lower() in GetExportFormats()
|
||||
|
||||
|
||||
def MakeBomTemplate(fmt):
|
||||
""" Generate a Bill of Materials upload template file (for user download) """
|
||||
|
||||
"""Generate a Bill of Materials upload template file (for user download)"""
|
||||
fmt = fmt.strip().lower()
|
||||
|
||||
if not IsValidBOMFormat(fmt):
|
||||
@ -45,13 +42,12 @@ def MakeBomTemplate(fmt):
|
||||
|
||||
|
||||
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False, manufacturer_data=False):
|
||||
""" Export a BOM (Bill of Materials) for a given part.
|
||||
"""Export a BOM (Bill of Materials) for a given part.
|
||||
|
||||
Args:
|
||||
fmt: File format (default = 'csv')
|
||||
cascade: If True, multi-level BOM output is supported. Otherwise, a flat top-level-only BOM is exported.
|
||||
"""
|
||||
|
||||
if not IsValidBOMFormat(fmt):
|
||||
fmt = 'csv'
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django Forms for interacting with Part objects
|
||||
"""
|
||||
"""Django Forms for interacting with Part objects"""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -19,7 +17,7 @@ from .models import (Part, PartCategory, PartCategoryParameterTemplate,
|
||||
|
||||
|
||||
class PartModelChoiceField(forms.ModelChoiceField):
|
||||
""" Extending string representation of Part instance with available stock """
|
||||
"""Extending string representation of Part instance with available stock"""
|
||||
|
||||
def label_from_instance(self, part):
|
||||
|
||||
@ -33,9 +31,7 @@ class PartModelChoiceField(forms.ModelChoiceField):
|
||||
|
||||
|
||||
class PartImageDownloadForm(HelperForm):
|
||||
"""
|
||||
Form for downloading an image from a URL
|
||||
"""
|
||||
"""Form for downloading an image from a URL"""
|
||||
|
||||
url = forms.URLField(
|
||||
label=_('URL'),
|
||||
@ -51,10 +47,10 @@ class PartImageDownloadForm(HelperForm):
|
||||
|
||||
|
||||
class BomMatchItemForm(MatchItemForm):
|
||||
""" Override MatchItemForm fields """
|
||||
"""Override MatchItemForm fields"""
|
||||
|
||||
def get_special_field(self, col_guess, row, file_manager):
|
||||
""" Set special fields """
|
||||
"""Set special fields"""
|
||||
|
||||
# set quantity field
|
||||
if 'quantity' in col_guess.lower():
|
||||
@ -74,13 +70,13 @@ class BomMatchItemForm(MatchItemForm):
|
||||
|
||||
|
||||
class SetPartCategoryForm(forms.Form):
|
||||
""" Form for setting the category of multiple Part objects """
|
||||
"""Form for setting the category of multiple Part objects"""
|
||||
|
||||
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
|
||||
|
||||
|
||||
class EditPartParameterTemplateForm(HelperForm):
|
||||
""" Form for editing a PartParameterTemplate object """
|
||||
"""Form for editing a PartParameterTemplate object"""
|
||||
|
||||
class Meta:
|
||||
model = PartParameterTemplate
|
||||
@ -91,7 +87,7 @@ class EditPartParameterTemplateForm(HelperForm):
|
||||
|
||||
|
||||
class EditCategoryParameterTemplateForm(HelperForm):
|
||||
""" Form for editing a PartCategoryParameterTemplate object """
|
||||
"""Form for editing a PartCategoryParameterTemplate object"""
|
||||
|
||||
add_to_same_level_categories = forms.BooleanField(required=False,
|
||||
initial=False,
|
||||
@ -113,7 +109,7 @@ class EditCategoryParameterTemplateForm(HelperForm):
|
||||
|
||||
|
||||
class PartPriceForm(forms.Form):
|
||||
""" Simple form for viewing part pricing information """
|
||||
"""Simple form for viewing part pricing information"""
|
||||
|
||||
quantity = forms.IntegerField(
|
||||
required=True,
|
||||
@ -130,9 +126,7 @@ class PartPriceForm(forms.Form):
|
||||
|
||||
|
||||
class EditPartSalePriceBreakForm(HelperForm):
|
||||
"""
|
||||
Form for creating / editing a sale price for a part
|
||||
"""
|
||||
"""Form for creating / editing a sale price for a part"""
|
||||
|
||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||
|
||||
@ -146,9 +140,7 @@ class EditPartSalePriceBreakForm(HelperForm):
|
||||
|
||||
|
||||
class EditPartInternalPriceBreakForm(HelperForm):
|
||||
"""
|
||||
Form for creating / editing a internal price for a part
|
||||
"""
|
||||
"""Form for creating / editing a internal price for a part"""
|
||||
|
||||
quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user