Exemplo n.º 1
0
class SupplierPriceBreak(models.Model):
    """ 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)

    Attributes:
        part: Link to a SupplierPart object that this price break applies to
        quantity: Quantity required for price break
        cost: Cost at specified quantity
        currency: Reference to the currency of this pricebreak (leave empty for base currency)
    """

    part = models.ForeignKey(SupplierPart,
                             on_delete=models.CASCADE,
                             related_name='pricebreaks')

    quantity = RoundingDecimalField(max_digits=15,
                                    decimal_places=5,
                                    default=1,
                                    validators=[MinValueValidator(1)])

    cost = RoundingDecimalField(max_digits=10,
                                decimal_places=5,
                                validators=[MinValueValidator(0)])

    currency = models.ForeignKey(Currency,
                                 blank=True,
                                 null=True,
                                 on_delete=models.SET_NULL)

    @property
    def converted_cost(self):
        """ Return the cost of this price break, converted to the base currency """

        scaler = Decimal(1.0)

        if self.currency:
            scaler = self.currency.value

        return self.cost * scaler

    class Meta:
        unique_together = ("part", "quantity")

        # This model was moved from the 'Part' app
        db_table = 'part_supplierpricebreak'

    def __str__(self):
        return "{mpn} - {cost} @ {quan}".format(mpn=self.part.MPN,
                                                cost=self.cost,
                                                quan=self.quantity)
Exemplo n.º 2
0
class OrderLineItem(models.Model):
    """ Abstract model for an order line item
    
    Attributes:
        quantity: Number of items
        note: Annotation for the item
        
    """
    class Meta:
        abstract = True

    quantity = RoundingDecimalField(max_digits=15,
                                    decimal_places=5,
                                    validators=[MinValueValidator(0)],
                                    default=1,
                                    verbose_name=_('Quantity'),
                                    help_text=_('Item quantity'))

    reference = models.CharField(max_length=100,
                                 blank=True,
                                 verbose_name=_('Reference'),
                                 help_text=_('Line item reference'))

    notes = models.CharField(max_length=500,
                             blank=True,
                             verbose_name=_('Notes'),
                             help_text=_('Line item notes'))
Exemplo n.º 3
0
class OrderLineItem(models.Model):
    """ Abstract model for an order line item

    Attributes:
        quantity: Number of items
        reference: Reference text (e.g. customer reference) for this line item
        note: Annotation for the item
        target_date: An (optional) date for expected shipment of this line item.
    """
    """
    Query filter for determining if an individual line item is "overdue":
    - Amount received is less than the required quantity
    - Target date is not None
    - Target date is in the past
    """
    OVERDUE_FILTER = Q(received__lt=F('quantity')) & ~Q(target_date=None) & Q(
        target_date__lt=datetime.now().date())

    class Meta:
        abstract = True

    quantity = RoundingDecimalField(
        verbose_name=_('Quantity'),
        help_text=_('Item quantity'),
        default=1,
        max_digits=15,
        decimal_places=5,
        validators=[MinValueValidator(0)],
    )

    reference = models.CharField(max_length=100,
                                 blank=True,
                                 verbose_name=_('Reference'),
                                 help_text=_('Line item reference'))

    notes = models.CharField(max_length=500,
                             blank=True,
                             verbose_name=_('Notes'),
                             help_text=_('Line item notes'))

    target_date = models.DateField(
        blank=True,
        null=True,
        verbose_name=_('Target Date'),
        help_text=_('Target shipping date for this line item'),
    )
Exemplo n.º 4
0
class SalesOrderAllocation(models.Model):
    """
    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.

    Attributes:
        line: SalesOrderLineItem reference
        item: StockItem reference
        quantity: Quantity to take from the StockItem

    """

    class Meta:
        unique_together = [
            # Cannot allocate any given StockItem to the same line more than once
            ('line', 'item'),
        ]

    def clean(self):
        """
        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
        - Allocated quantity cannot exceed the quantity of the stock item
        - Allocation quantity must be "1" if the StockItem is serialized
        - Allocation quantity cannot be zero
        """

        super().clean()

        errors = {}

        try:
            if not self.item:
                raise ValidationError({'item': _('Stock item has not been assigned')})
        except stock_models.StockItem.DoesNotExist:
            raise ValidationError({'item': _('Stock item has not been assigned')})

        try:
            if not self.line.part == self.item.part:
                errors['item'] = _('Cannot allocate stock item to a line with a different part')
        except PartModels.Part.DoesNotExist:
            errors['line'] = _('Cannot allocate stock to a line without a part')

        if self.quantity > self.item.quantity:
            errors['quantity'] = _('Allocation quantity cannot exceed stock quantity')

        # TODO: The logic here needs improving. Do we need to subtract our own amount, or something?
        if self.item.quantity - self.item.allocation_count() + self.quantity < self.quantity:
            errors['quantity'] = _('StockItem is over-allocated')

        if self.quantity <= 0:
            errors['quantity'] = _('Allocation quantity must be greater than zero')

        if self.item.serial and not self.quantity == 1:
            errors['quantity'] = _('Quantity must be 1 for serialized stock item')

        if len(errors) > 0:
            raise ValidationError(errors)

    line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, verbose_name=_('Line'), related_name='allocations')

    item = models.ForeignKey(
        'stock.StockItem',
        on_delete=models.CASCADE,
        related_name='sales_order_allocations',
        limit_choices_to={
            'part__salable': True,
            'belongs_to': None,
            'sales_order': None,
        },
        verbose_name=_('Item'),
        help_text=_('Select stock item to allocate')
    )

    quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1, verbose_name=_('Quantity'), help_text=_('Enter stock allocation quantity'))

    def get_serial(self):
        return self.item.serial

    def get_location(self):
        return self.item.location.id if self.item.location else None

    def get_location_path(self):
        if self.item.location:
            return self.item.location.pathstring
        else:
            return ""

    def complete_allocation(self, user):
        """
        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(
            order.customer,
            quantity=self.quantity,
            order=order,
            user=user
        )

        # Update our own reference to the StockItem
        # (It may have changed if the stock was split)
        self.item = item
        self.save()
Exemplo n.º 5
0
class SalesOrderLineItem(OrderLineItem):
    """
    Model for a single LineItem in a SalesOrder

    Attributes:
        order: Link to the SalesOrder that this line item belongs to
        part: Link to a Part object (may be null)
        sale_price: The unit sale price for this OrderLineItem
        shipped: The number of items which have actually shipped against this line item
    """

    @staticmethod
    def get_api_url():
        return reverse('api-so-line-list')

    order = models.ForeignKey(
        SalesOrder,
        on_delete=models.CASCADE,
        related_name='lines',
        verbose_name=_('Order'),
        help_text=_('Sales Order')
    )

    part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True})

    sale_price = InvenTreeModelMoneyField(
        max_digits=19,
        decimal_places=4,
        null=True, blank=True,
        verbose_name=_('Sale Price'),
        help_text=_('Unit sale price'),
    )

    shipped = RoundingDecimalField(
        verbose_name=_('Shipped'),
        help_text=_('Shipped quantity'),
        default=0,
        max_digits=15, decimal_places=5,
        validators=[MinValueValidator(0)]
    )

    class Meta:
        unique_together = [
        ]

    def fulfilled_quantity(self):
        """
        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.

        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 """

        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 self.allocated_quantity() > self.quantity

    def is_completed(self):
        """
        Return True if this line item is completed (has been fully shipped)
        """

        return self.shipped >= self.quantity