Example #1
0
def sales_order_status_label(key, *args, **kwargs):
    """ Render a SalesOrder status label """
    return mark_safe(SalesOrderStatus.render(key, large=kwargs.get('large', False)))
Example #2
0
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
Example #3
0
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
Example #4
0
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()
Example #5
0
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