Exemple #1
0
class PurchaseOrder(Order):
    """ A PurchaseOrder represents goods shipped inwards from an external supplier.

    Attributes:
        supplier: Reference to the company supplying the goods in the order
        supplier_reference: Optional field for supplier order reference code
        received_by: User that received the goods
        target_date: Expected delivery target date for PurchaseOrder completion (optional)
    """

    OVERDUE_FILTER = Q(status__in=PurchaseOrderStatus.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 bewteen these dates

        To be "interesting":
        - A "received" order where the received 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 "received" orders within the range
        received = Q(status=PurchaseOrderStatus.COMPLETE) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date)

        # Construct a queryset for "pending" orders within the range
        pending = Q(status__in=PurchaseOrderStatus.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(received | pending)

        return queryset

    def __str__(self):

        prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX')

        return f"{prefix}{self.reference} - {self.supplier.name}"

    status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
                                         help_text=_('Purchase order status'))

    supplier = models.ForeignKey(
        Company, on_delete=models.CASCADE,
        limit_choices_to={
            'is_supplier': True,
        },
        related_name='purchase_orders',
        verbose_name=_('Supplier'),
        help_text=_('Company from which the items are being ordered')
    )

    supplier_reference = models.CharField(max_length=64, blank=True, verbose_name=_('Supplier Reference'), help_text=_("Supplier order reference code"))

    received_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        blank=True, null=True,
        related_name='+',
        verbose_name=_('received by')
    )

    issue_date = models.DateField(
        blank=True, null=True,
        verbose_name=_('Issue Date'),
        help_text=_('Date order was issued')
    )

    target_date = models.DateField(
        blank=True, null=True,
        verbose_name=_('Target Delivery Date'),
        help_text=_('Expected date for order delivery. Order will be overdue after this date.'),
    )

    complete_date = models.DateField(
        blank=True, null=True,
        verbose_name=_('Completion Date'),
        help_text=_('Date order was completed')
    )

    def get_absolute_url(self):
        return reverse('po-detail', kwargs={'pk': self.id})

    @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.
        This function will check that:

        * The supplier part matches the supplier specified for this purchase order
        * The quantity is greater than zero

        Args:
            supplier_part - The supplier_part to add
            quantity - The number of items to add
            group - If True, this new quantity will be added to an existing line item for the same supplier_part (if it exists)
        """

        try:
            quantity = int(quantity)
            if quantity <= 0:
                raise ValidationError({
                    'quantity': _("Quantity must be greater than zero")})
        except ValueError:
            raise ValidationError({'quantity': _("Invalid quantity provided")})

        if not supplier_part.supplier == self.supplier:
            raise ValidationError({'supplier': _("Part supplier must match PO supplier")})

        if group:
            # Check if there is already a matching line item (for this PO)
            matches = self.lines.filter(part=supplier_part)

            if matches.count() > 0:
                line = matches.first()

                # update quantity and price
                quantity_new = line.quantity + quantity
                line.quantity = quantity_new
                supplier_price = supplier_part.get_price(quantity_new)
                if line.purchase_price and supplier_price:
                    line.purchase_price = supplier_price / quantity_new
                line.save()

                return

        line = PurchaseOrderLineItem(
            order=self,
            part=supplier_part,
            quantity=quantity,
            reference=reference,
            purchase_price=purchase_price,
        )

        line.save()

    @transaction.atomic
    def place_order(self):
        """ 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()
            self.save()

    @transaction.atomic
    def complete_order(self):
        """ 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()
            self.save()

    @property
    def is_overdue(self):
        """
        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:
        """

        return self.status in [
            PurchaseOrderStatus.PLACED,
            PurchaseOrderStatus.PENDING
        ]

    def cancel_order(self):
        """ Marks the PurchaseOrder as CANCELLED. """

        if self.can_cancel():
            self.status = PurchaseOrderStatus.CANCELLED
            self.save()

    def pending_line_items(self):
        """ 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'))

    @property
    def is_complete(self):
        """ Return True if all line items have been received """

        return self.pending_line_items().count() == 0

    @transaction.atomic
    def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None, **kwargs):
        """ Receive a line item (or partial line item) against this PO
        """

        notes = kwargs.get('notes', '')

        if not self.status == PurchaseOrderStatus.PLACED:
            raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})

        try:
            if not (quantity % 1 == 0):
                raise ValidationError({"quantity": _("Quantity must be an integer")})
            if quantity < 0:
                raise ValidationError({"quantity": _("Quantity must be a positive number")})
            quantity = int(quantity)
        except (ValueError, TypeError):
            raise ValidationError({"quantity": _("Invalid quantity provided")})

        # Create a new stock item
        if line.part and quantity > 0:
            stock = stock_models.StockItem(
                part=line.part.part,
                supplier_part=line.part,
                location=location,
                quantity=quantity,
                purchase_order=self,
                status=status,
                purchase_price=purchase_price,
            )

            stock.save(add_note=False)

            tracking_info = {
                'status': status,
                'purchaseorder': self.pk,
            }

            stock.add_tracking_entry(
                StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER,
                user,
                notes=notes,
                deltas=tracking_info,
                location=location,
                purchaseorder=self,
                quantity=quantity
            )

        # Update the number of parts received against the particular line item
        line.received += quantity
        line.save()

        # Has this order been completed?
        if len(self.pending_line_items()) == 0:

            self.received_by = user
            self.complete_order()  # This will save the model
Exemple #2
0
class PurchaseOrder(Order):
    """ A PurchaseOrder represents goods shipped inwards from an external supplier.

    Attributes:
        supplier: Reference to the company supplying the goods in the order
        supplier_reference: Optional field for supplier order reference code
        received_by: User that received the goods
    """
    
    ORDER_PREFIX = "PO"

    def __str__(self):
        return "PO {ref} - {company}".format(ref=self.reference, company=self.supplier.name)

    status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
                                         help_text='Purchase order status')

    supplier = models.ForeignKey(
        Company, on_delete=models.CASCADE,
        limit_choices_to={
            'is_supplier': True,
        },
        related_name='purchase_orders',
        help_text=_('Supplier')
    )

    supplier_reference = models.CharField(max_length=64, blank=True, help_text=_("Supplier order reference code"))

    received_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        blank=True, null=True,
        related_name='+'
    )

    issue_date = models.DateField(blank=True, null=True)

    complete_date = models.DateField(blank=True, null=True)

    def get_absolute_url(self):
        return reverse('po-detail', kwargs={'pk': self.id})

    @transaction.atomic
    def add_line_item(self, supplier_part, quantity, group=True, reference=''):
        """ 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
        * The quantity is greater than zero

        Args:
            supplier_part - The supplier_part to add
            quantity - The number of items to add
            group - If True, this new quantity will be added to an existing line item for the same supplier_part (if it exists)
        """

        try:
            quantity = int(quantity)
            if quantity <= 0:
                raise ValidationError({
                    'quantity': _("Quantity must be greater than zero")})
        except ValueError:
            raise ValidationError({'quantity': _("Invalid quantity provided")})

        if not supplier_part.supplier == self.supplier:
            raise ValidationError({'supplier': _("Part supplier must match PO supplier")})

        if group:
            # Check if there is already a matching line item (for this PO)
            matches = self.lines.filter(part=supplier_part)

            if matches.count() > 0:
                line = matches.first()

                line.quantity += quantity
                line.save()

                return

        line = PurchaseOrderLineItem(
            order=self,
            part=supplier_part,
            quantity=quantity,
            reference=reference)

        line.save()

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

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

    def cancel_order(self):
        """ Marks the PurchaseOrder as CANCELLED. """

        if self.status in [PurchaseOrderStatus.PLACED, PurchaseOrderStatus.PENDING]:
            self.status = PurchaseOrderStatus.CANCELLED
            self.save()

    def pending_line_items(self):
        """ 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'))

    @property
    def is_complete(self):
        """ Return True if all line items have been received """

        return self.pending_line_items().count() == 0

    @transaction.atomic
    def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK):
        """ Receive a line item (or partial line item) against this PO
        """

        if not self.status == PurchaseOrderStatus.PLACED:
            raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})

        try:
            quantity = int(quantity)
            if quantity <= 0:
                raise ValidationError({"quantity": _("Quantity must be greater than zero")})
        except ValueError:
            raise ValidationError({"quantity": _("Invalid quantity provided")})

        # Create a new stock item
        if line.part:
            stock = stock_models.StockItem(
                part=line.part.part,
                supplier_part=line.part,
                location=location,
                quantity=quantity,
                purchase_order=self,
                status=status
            )

            stock.save()

            # Add a new transaction note to the newly created stock item
            stock.addTransactionNote("Received items", user, "Received {q} items against order '{po}'".format(
                q=quantity,
                po=str(self))
            )

        # Update the number of parts received against the particular line item
        line.received += quantity
        line.save()

        # Has this order been completed?
        if len(self.pending_line_items()) == 0:
            
            self.received_by = user
            self.complete_order()  # This will save the model