def sales_order_status_label(key, *args, **kwargs): """ Render a SalesOrder status label """ return mark_safe(SalesOrderStatus.render(key, large=kwargs.get('large', False)))
class SalesOrder(Order): """ A SalesOrder represents a list of goods shipped outwards to a customer. Attributes: customer: Reference to the company receiving the goods in the order customer_reference: Optional field for customer order reference code target_date: Target date for SalesOrder completion (optional) """ OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) @staticmethod def filterByDate(queryset, min_date, max_date): """ Filter by "minimum and maximum date range" - Specified as min_date, max_date - Both must be specified for filter to be applied - Determine which "interesting" orders exist between these dates To be "interesting": - A "completed" order where the completion date lies within the date range - A "pending" order where the target date lies within the date range - TODO: An "overdue" order where the target date is in the past """ date_fmt = '%Y-%m-%d' # ISO format date string # Ensure that both dates are valid try: min_date = datetime.strptime(str(min_date), date_fmt).date() max_date = datetime.strptime(str(max_date), date_fmt).date() except (ValueError, TypeError): # Date processing error, return queryset unchanged return queryset # Construct a queryset for "completed" orders within the range completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date) # Construct a queryset for "pending" orders within the range pending = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date) # TODO: Construct a queryset for "overdue" orders within the range queryset = queryset.filter(completed | pending) return queryset def __str__(self): prefix = getSetting('SALESORDER_REFERENCE_PREFIX') return f"{prefix}{self.reference} - {self.customer.name}" def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) customer = models.ForeignKey( Company, on_delete=models.SET_NULL, null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', verbose_name=_('Customer'), help_text=_("Company to which the items are being sold"), ) status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(), verbose_name=_('Status'), help_text=_('Purchase order status')) customer_reference = models.CharField(max_length=64, blank=True, verbose_name=_('Customer Reference '), help_text=_("Customer order reference code")) target_date = models.DateField( null=True, blank=True, verbose_name=_('Target completion date'), help_text=_('Target date for order completion. Order will be overdue after this date.') ) shipment_date = models.DateField(blank=True, null=True, verbose_name=_('Shipment Date')) shipped_by = models.ForeignKey( User, on_delete=models.SET_NULL, blank=True, null=True, related_name='+', verbose_name=_('shipped by') ) @property def is_overdue(self): """ 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) return query.exists() @property def is_pending(self): return self.status == SalesOrderStatus.PENDING def is_fully_allocated(self): """ Return True if all line items are fully allocated """ for line in self.lines.all(): if not line.is_fully_allocated(): return False return True def is_over_allocated(self): """ Return true if any lines in the order are over-allocated """ for line in self.lines.all(): if line.is_over_allocated(): return True return False @transaction.atomic def ship_order(self, user): """ Mark this order as 'shipped' """ # The order can only be 'shipped' if the current status is PENDING if not self.status == SalesOrderStatus.PENDING: raise ValidationError({'status': _("SalesOrder cannot be shipped as it is not currently pending")}) # Complete the allocation for each allocated StockItem for line in self.lines.all(): for allocation in line.allocations.all(): allocation.complete_allocation(user) # Remove the allocation from the database once it has been 'fulfilled' if allocation.item.sales_order == self: allocation.delete() else: raise ValidationError("Could not complete order - allocation item not fulfilled") # Ensure the order status is marked as "Shipped" self.status = SalesOrderStatus.SHIPPED self.shipment_date = datetime.now().date() self.shipped_by = user self.save() return True def can_cancel(self): """ Return True if this order can be cancelled """ if not self.status == SalesOrderStatus.PENDING: return False return True @transaction.atomic def cancel_order(self): """ 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 self.status = SalesOrderStatus.CANCELLED self.save() for line in self.lines.all(): for allocation in line.allocations.all(): allocation.delete() return True
class SalesOrder(Order): """ A SalesOrder represents a list of goods shipped outwards to a customer. Attributes: customer: Reference to the company receiving the goods in the order customer_reference: Optional field for customer order reference code """ def __str__(self): return "SO {ref} - {company}".format(ref=self.reference, company=self.customer.name) def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) customer = models.ForeignKey( Company, on_delete=models.SET_NULL, null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', help_text=_("Customer"), ) status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(), help_text='Purchase order status') customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code")) shipment_date = models.DateField(blank=True, null=True) shipped_by = models.ForeignKey( User, on_delete=models.SET_NULL, blank=True, null=True, related_name='+' ) @property def is_pending(self): return self.status == SalesOrderStatus.PENDING def is_fully_allocated(self): """ Return True if all line items are fully allocated """ for line in self.lines.all(): if not line.is_fully_allocated(): return False return True def is_over_allocated(self): """ Return true if any lines in the order are over-allocated """ for line in self.lines.all(): if line.is_over_allocated(): return True return False @transaction.atomic def ship_order(self, user): """ Mark this order as 'shipped' """ # The order can only be 'shipped' if the current status is PENDING if not self.status == SalesOrderStatus.PENDING: raise ValidationError({'status': _("SalesOrder cannot be shipped as it is not currently pending")}) # Complete the allocation for each allocated StockItem for line in self.lines.all(): for allocation in line.allocations.all(): allocation.complete_allocation(user) # Remove the allocation from the database once it has been 'fulfilled' if allocation.item.sales_order == self: allocation.delete() else: raise ValidationError("Could not complete order - allocation item not fulfilled") # Ensure the order status is marked as "Shipped" self.status = SalesOrderStatus.SHIPPED self.shipment_date = datetime.now().date() self.shipped_by = user self.save() return True @transaction.atomic def cancel_order(self): """ Cancel this order (only if it is "pending") - Mark the order as 'cancelled' - Delete any StockItems which have been allocated """ if not self.status == SalesOrderStatus.PENDING: return False self.status = SalesOrderStatus.CANCELLED self.save() for line in self.lines.all(): for allocation in line.allocations.all(): allocation.delete() return True
class SalesOrder(Order): """ A SalesOrder represents a list of goods shipped outwards to a customer. Attributes: customer: Reference to the company receiving the goods in the order customer_reference: Optional field for customer order reference code target_date: Target date for SalesOrder completion (optional) """ @staticmethod def get_api_url(): return reverse('api-so-list') OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) @staticmethod def filterByDate(queryset, min_date, max_date): """ Filter by "minimum and maximum date range" - Specified as min_date, max_date - Both must be specified for filter to be applied - Determine which "interesting" orders exist between these dates To be "interesting": - A "completed" order where the completion date lies within the date range - A "pending" order where the target date lies within the date range - TODO: An "overdue" order where the target date is in the past """ date_fmt = '%Y-%m-%d' # ISO format date string # Ensure that both dates are valid try: min_date = datetime.strptime(str(min_date), date_fmt).date() max_date = datetime.strptime(str(max_date), date_fmt).date() except (ValueError, TypeError): # Date processing error, return queryset unchanged return queryset # Construct a queryset for "completed" orders within the range completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date) # Construct a queryset for "pending" orders within the range pending = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date) # TODO: Construct a queryset for "overdue" orders within the range queryset = queryset.filter(completed | pending) return queryset def save(self, *args, **kwargs): self.rebuild_reference_field() super().save(*args, **kwargs) def __str__(self): prefix = getSetting('SALESORDER_REFERENCE_PREFIX') return f"{prefix}{self.reference} - {self.customer.name}" def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) reference = models.CharField( unique=True, max_length=64, blank=False, verbose_name=_('Reference'), help_text=_('Order reference'), default=get_next_so_number, ) customer = models.ForeignKey( Company, on_delete=models.SET_NULL, null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', verbose_name=_('Customer'), help_text=_("Company to which the items are being sold"), ) status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(), verbose_name=_('Status'), help_text=_('Purchase order status')) customer_reference = models.CharField(max_length=64, blank=True, verbose_name=_('Customer Reference '), help_text=_("Customer order reference code")) target_date = models.DateField( null=True, blank=True, verbose_name=_('Target completion date'), help_text=_('Target date for order completion. Order will be overdue after this date.') ) shipment_date = models.DateField(blank=True, null=True, verbose_name=_('Shipment Date')) shipped_by = models.ForeignKey( User, on_delete=models.SET_NULL, blank=True, null=True, related_name='+', verbose_name=_('shipped by') ) @property def is_overdue(self): """ 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) return query.exists() @property def is_pending(self): return self.status == SalesOrderStatus.PENDING @property def stock_allocations(self): """ 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 """ for line in self.lines.all(): if not line.is_fully_allocated(): return False return True def is_over_allocated(self): """ Return true if any lines in the order are over-allocated """ for line in self.lines.all(): if line.is_over_allocated(): return True return False def is_completed(self): """ 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. Throws a ValidationError if cannot be completed. """ # Order without line items cannot be completed if self.lines.count() == 0: if raise_error: raise ValidationError(_('Order cannot be completed as no parts have been assigned')) # Only a PENDING order can be marked as SHIPPED elif self.status != SalesOrderStatus.PENDING: if raise_error: raise ValidationError(_('Only a pending order can be marked as complete')) elif self.pending_shipment_count > 0: if raise_error: raise ValidationError(_("Order cannot be completed as there are incomplete shipments")) elif self.pending_line_count > 0: if raise_error: raise ValidationError(_("Order cannot be completed as there are incomplete line items")) else: return True return False def complete_order(self, user): """ Mark this order as "complete" """ if not self.can_complete(): return False self.status = SalesOrderStatus.SHIPPED self.shipped_by = user self.shipment_date = datetime.now() self.save() return True def can_cancel(self): """ Return True if this order can be cancelled """ if not self.status == SalesOrderStatus.PENDING: return False return True @transaction.atomic def cancel_order(self): """ 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 self.status = SalesOrderStatus.CANCELLED self.save() for line in self.lines.all(): for allocation in line.allocations.all(): allocation.delete() return True @property def line_count(self): return self.lines.count() def completed_line_items(self): """ 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 self.lines.filter(shipped__lt=F('quantity')) @property def completed_line_count(self): return self.completed_line_items().count() @property def pending_line_count(self): return self.pending_line_items().count() def completed_shipments(self): """ 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 self.shipments.filter(shipment_date=None) @property def shipment_count(self): return self.shipments.count() @property def completed_shipment_count(self): return self.completed_shipments().count() @property def pending_shipment_count(self): return self.pending_shipments().count()
class SalesOrder(Order): """ A SalesOrder represents a list of goods shipped outwards to a customer. Attributes: customer: Reference to the company receiving the goods in the order customer_reference: Optional field for customer order reference code target_date: Target date for SalesOrder completion (optional) """ OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q( target_date=None) & Q(target_date__lte=datetime.now().date()) def __str__(self): prefix = getSetting('SALESORDER_REFERENCE_PREFIX') return f"{prefix}{self.reference} - {self.customer.name}" def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) customer = models.ForeignKey( Company, on_delete=models.SET_NULL, null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', help_text=_("Company to which the items are being sold"), ) status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(), help_text=_('Purchase order status')) customer_reference = models.CharField( max_length=64, blank=True, help_text=_("Customer order reference code")) target_date = models.DateField( null=True, blank=True, verbose_name=_('Target completion date'), help_text= _('Target date for order completion. Order will be overdue after this date.' )) shipment_date = models.DateField(blank=True, null=True) shipped_by = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name='+') @property def is_overdue(self): """ Returns true if this SalesOrder is "overdue": - Not completed - Target date is "in the past" """ # Order cannot be deemed overdue if target_date is not set if self.target_date is None: return False today = datetime.now().date() return self.is_pending and self.target_date < today @property def is_pending(self): return self.status == SalesOrderStatus.PENDING def is_fully_allocated(self): """ Return True if all line items are fully allocated """ for line in self.lines.all(): if not line.is_fully_allocated(): return False return True def is_over_allocated(self): """ Return true if any lines in the order are over-allocated """ for line in self.lines.all(): if line.is_over_allocated(): return True return False @transaction.atomic def ship_order(self, user): """ Mark this order as 'shipped' """ # The order can only be 'shipped' if the current status is PENDING if not self.status == SalesOrderStatus.PENDING: raise ValidationError({ 'status': _("SalesOrder cannot be shipped as it is not currently pending" ) }) # Complete the allocation for each allocated StockItem for line in self.lines.all(): for allocation in line.allocations.all(): allocation.complete_allocation(user) # Remove the allocation from the database once it has been 'fulfilled' if allocation.item.sales_order == self: allocation.delete() else: raise ValidationError( "Could not complete order - allocation item not fulfilled" ) # Ensure the order status is marked as "Shipped" self.status = SalesOrderStatus.SHIPPED self.shipment_date = datetime.now().date() self.shipped_by = user self.save() return True def can_cancel(self): """ Return True if this order can be cancelled """ if not self.status == SalesOrderStatus.PENDING: return False return True @transaction.atomic def cancel_order(self): """ 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 self.status = SalesOrderStatus.CANCELLED self.save() for line in self.lines.all(): for allocation in line.allocations.all(): allocation.delete() return True