예제 #1
0
class Build(MPTTModel):
    """ A Build object organises the creation of new StockItem objects from other existing StockItem objects.

    Attributes:
        part: The part to be built (from component BOM items)
        reference: Build order reference (required, must be unique)
        title: Brief title describing the build (required)
        quantity: Number of units to be built
        parent: Reference to a Build object for which this Build is required
        sales_order: References to a SalesOrder object for which this Build is required (e.g. the output of this build will be used to fulfil a sales order)
        take_from: Location to take stock from to make this build (if blank, can take from anywhere)
        status: Build status code
        batch: Batch code transferred to build parts (optional)
        creation_date: Date the build was created (auto)
        target_date: Date the build will be overdue
        completion_date: Date the build was completed (or, if incomplete, the expected date of completion)
        link: External URL for extra information
        notes: Text notes
    """

    OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(
        target_date=None) & Q(target_date__lte=datetime.now().date())

    class Meta:
        verbose_name = _("Build Order")
        verbose_name_plural = _("Build Orders")

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

        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

        # Order was completed within the specified range
        completed = Q(status=BuildStatus.COMPLETE) & Q(
            completion_date__gte=min_date) & Q(completion_date__lte=max_date)

        # Order target date falls witin specified range
        pending = Q(
            status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(
                target_date__gte=min_date) & Q(target_date__lte=max_date)

        # TODO - Construct a queryset for "overdue" orders

        queryset = queryset.filter(completed | pending)

        return queryset

    def __str__(self):

        prefix = getSetting("BUILDORDER_REFERENCE_PREFIX")

        return f"{prefix}{self.reference}"

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

    reference = models.CharField(unique=True,
                                 max_length=64,
                                 blank=False,
                                 help_text=_('Build Order Reference'),
                                 verbose_name=_('Reference'),
                                 validators=[validate_build_order_reference])

    title = models.CharField(verbose_name=_('Description'),
                             blank=False,
                             max_length=100,
                             help_text=_('Brief description of the build'))

    # TODO - Perhaps delete the build "tree"
    parent = TreeForeignKey(
        'self',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='children',
        verbose_name=_('Parent Build'),
        help_text=_('BuildOrder to which this build is allocated'),
    )

    part = models.ForeignKey(
        'part.Part',
        verbose_name=_('Part'),
        on_delete=models.CASCADE,
        related_name='builds',
        limit_choices_to={
            'assembly': True,
            'active': True,
            'virtual': False,
        },
        help_text=_('Select part to build'),
    )

    sales_order = models.ForeignKey(
        'order.SalesOrder',
        verbose_name=_('Sales Order Reference'),
        on_delete=models.SET_NULL,
        related_name='builds',
        null=True,
        blank=True,
        help_text=_('SalesOrder to which this build is allocated'))

    take_from = models.ForeignKey(
        'stock.StockLocation',
        verbose_name=_('Source Location'),
        on_delete=models.SET_NULL,
        related_name='sourcing_builds',
        null=True,
        blank=True,
        help_text=
        _('Select location to take stock from for this build (leave blank to take from any stock location)'
          ))

    destination = models.ForeignKey(
        'stock.StockLocation',
        verbose_name=_('Destination Location'),
        on_delete=models.SET_NULL,
        related_name='incoming_builds',
        null=True,
        blank=True,
        help_text=_(
            'Select location where the completed items will be stored'),
    )

    quantity = models.PositiveIntegerField(
        verbose_name=_('Build Quantity'),
        default=1,
        validators=[MinValueValidator(1)],
        help_text=_('Number of stock items to build'))

    completed = models.PositiveIntegerField(
        verbose_name=_('Completed items'),
        default=0,
        help_text=_('Number of stock items which have been completed'))

    status = models.PositiveIntegerField(verbose_name=_('Build Status'),
                                         default=BuildStatus.PENDING,
                                         choices=BuildStatus.items(),
                                         validators=[MinValueValidator(0)],
                                         help_text=_('Build status code'))

    batch = models.CharField(verbose_name=_('Batch Code'),
                             max_length=100,
                             blank=True,
                             null=True,
                             help_text=_('Batch code for this build output'))

    creation_date = models.DateField(auto_now_add=True, editable=False)

    target_date = models.DateField(
        null=True,
        blank=True,
        verbose_name=_('Target completion date'),
        help_text=
        _('Target date for build completion. Build will be overdue after this date.'
          ))

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

    completed_by = models.ForeignKey(User,
                                     on_delete=models.SET_NULL,
                                     blank=True,
                                     null=True,
                                     related_name='builds_completed')

    link = InvenTree.fields.InvenTreeURLField(
        verbose_name=_('External Link'),
        blank=True,
        help_text=_('Link to external URL'))

    notes = MarkdownxField(verbose_name=_('Notes'),
                           blank=True,
                           help_text=_('Extra build notes'))

    def is_overdue(self):
        """
        Returns true if this build is "overdue":

        - Not completed
        - Target date is "in the past"
        """

        # Cannot be deemed overdue if target_date is not set
        if self.target_date is None:
            return False

        today = datetime.now().date()

        return self.active and self.target_date < today

    @property
    def active(self):
        """
        Return True if this build is active
        """

        return self.status in BuildStatus.ACTIVE_CODES

    @property
    def bom_items(self):
        """
        Returns the BOM items for the part referenced by this BuildOrder
        """

        return self.part.bom_items.all().prefetch_related('sub_part')

    @property
    def remaining(self):
        """
        Return the number of outputs remaining to be completed.
        """

        return max(0, self.quantity - self.completed)

    @property
    def output_count(self):
        return self.build_outputs.count()

    def get_build_outputs(self, **kwargs):
        """
        Return a list of build outputs.

        kwargs:
            complete = (True / False) - If supplied, filter by completed status
            in_stock = (True / False) - If supplied, filter by 'in-stock' status
        """

        outputs = self.build_outputs.all()

        # Filter by 'in stock' status
        in_stock = kwargs.get('in_stock', None)

        if in_stock is not None:
            if in_stock:
                outputs = outputs.filter(StockModels.StockItem.IN_STOCK_FILTER)
            else:
                outputs = outputs.exclude(
                    StockModels.StockItem.IN_STOCK_FILTER)

        # Filter by 'complete' status
        complete = kwargs.get('complete', None)

        if complete is not None:
            if complete:
                outputs = outputs.filter(is_building=False)
            else:
                outputs = outputs.filter(is_building=True)

        return outputs

    @property
    def complete_outputs(self):
        """
        Return all the "completed" build outputs
        """

        outputs = self.get_build_outputs(complete=True)

        # TODO - Ordering?

        return outputs

    @property
    def incomplete_outputs(self):
        """
        Return all the "incomplete" build outputs
        """

        outputs = self.get_build_outputs(complete=False)

        # TODO - Order by how "complete" they are?

        return outputs

    @property
    def incomplete_count(self):
        """
        Return the total number of "incomplete" outputs
        """

        quantity = 0

        for output in self.incomplete_outputs:
            quantity += output.quantity

        return quantity

    @classmethod
    def getNextBuildNumber(cls):
        """
        Try to predict the next Build Order reference:
        """

        if cls.objects.count() == 0:
            return None

        build = cls.objects.last()
        ref = build.reference

        if not ref:
            return None

        tries = set()

        while 1:
            new_ref = increment(ref)

            if new_ref in tries:
                # We are potentially stuck in a loop - simply return the original reference
                return ref

            if cls.objects.filter(reference=new_ref).exists():
                tries.add(new_ref)
                new_ref = increment(new_ref)
            else:
                break

        return new_ref

    @property
    def can_complete(self):
        """
        Returns True if this build can be "completed"

        - Must not have any outstanding build outputs
        - 'completed' value must meet (or exceed) the 'quantity' value
        """

        if self.incomplete_count > 0:
            return False

        if self.completed < self.quantity:
            return False

        # No issues!
        return True

    @transaction.atomic
    def complete_build(self, user):
        """
        Mark this build as complete
        """

        if not self.can_complete:
            return

        self.completion_date = datetime.now().date()
        self.completed_by = user
        self.status = BuildStatus.COMPLETE
        self.save()

        # Ensure that there are no longer any BuildItem objects
        # which point to thie Build Order
        self.allocated_stock.all().delete()

    @transaction.atomic
    def cancelBuild(self, user):
        """ Mark the Build as CANCELLED

        - Delete any pending BuildItem objects (but do not remove items from stock)
        - Set build status to CANCELLED
        - Save the Build object
        """

        for item in self.allocated_stock.all():
            item.delete()

        # Date of 'completion' is the date the build was cancelled
        self.completion_date = datetime.now().date()
        self.completed_by = user

        self.status = BuildStatus.CANCELLED
        self.save()

    def getAutoAllocations(self, output):
        """
        Return a list of StockItem objects which will be allocated
        using the 'AutoAllocate' function.

        For each item in the BOM for the attached Part,
        the following tests must *all* evaluate to True,
        for the part to be auto-allocated:

        - The sub_item in the BOM line must *not* be trackable
        - There is only a single stock item available (which has not already been allocated to this build)
        - The stock item has an availability greater than zero
        
        Returns:
            A list object containing the StockItem objects to be allocated (and the quantities).
            Each item in the list is a dict as follows:
            {
                'stock_item': stock_item,
                'quantity': stock_quantity,
            }
        """

        allocations = []
        """
        Iterate through each item in the BOM
        """

        for bom_item in self.bom_items:

            part = bom_item.sub_part

            # Skip any parts which are already fully allocated
            if self.isPartFullyAllocated(part, output):
                continue

            # How many parts are required to complete the output?
            required = self.unallocatedQuantity(part, output)

            # Grab a list of stock items which are available
            stock_items = self.availableStockItems(part, output)

            # Ensure that the available stock items are in the correct location
            if self.take_from is not None:
                # Filter for stock that is located downstream of the designated location
                stock_items = stock_items.filter(location__in=[
                    loc for loc in self.take_from.getUniqueChildren()
                ])

            # Only one StockItem to choose from? Default to that one!
            if stock_items.count() == 1:
                stock_item = stock_items[0]

                # Double check that we have not already allocated this stock-item against this build
                build_items = BuildItem.objects.filter(build=self,
                                                       stock_item=stock_item,
                                                       install_into=output)

                if len(build_items) > 0:
                    continue

                # How many items are actually available?
                if stock_item.quantity > 0:

                    # Only take as many as are available
                    if stock_item.quantity < required:
                        required = stock_item.quantity

                    allocation = {
                        'stock_item': stock_item,
                        'quantity': required,
                    }

                    allocations.append(allocation)

        return allocations

    @transaction.atomic
    def unallocateStock(self, output=None, part=None):
        """
        Deletes all stock allocations for this build.
        
        Args:
            output: Specify which build output to delete allocations (optional)

        """

        allocations = BuildItem.objects.filter(build=self.pk)

        if output:
            allocations = allocations.filter(install_into=output.pk)

        if part:
            allocations = allocations.filter(stock_item__part=part)

        # Remove all the allocations
        allocations.delete()

    @transaction.atomic
    def create_build_output(self, quantity, **kwargs):
        """
        Create a new build output against this BuildOrder.

        args:
            quantity: The quantity of the item to produce

        kwargs:
            batch: Override batch code
            serials: Serial numbers
            location: Override location
        """

        batch = kwargs.get('batch', self.batch)
        location = kwargs.get('location', self.destination)
        serials = kwargs.get('serials', None)
        """
        Determine if we can create a single output (with quantity > 0),
        or multiple outputs (with quantity = 1)
        """

        multiple = False

        # Serial numbers are provided? We need to split!
        if serials:
            multiple = True

        # BOM has trackable parts, so we must split!
        if self.part.has_trackable_parts:
            multiple = True

        if multiple:
            """
            Create multiple build outputs with a single quantity of 1
            """

            for ii in range(quantity):

                if serials:
                    serial = serials[ii]
                else:
                    serial = None

                StockModels.StockItem.objects.create(
                    quantity=1,
                    location=location,
                    part=self.part,
                    build=self,
                    batch=batch,
                    serial=serial,
                    is_building=True,
                )

        else:
            """
            Create a single build output of the given quantity
            """

            StockModels.StockItem.objects.create(quantity=quantity,
                                                 location=location,
                                                 part=self.part,
                                                 build=self,
                                                 batch=batch,
                                                 is_building=True)

        if self.status == BuildStatus.PENDING:
            self.status = BuildStatus.PRODUCTION
            self.save()

    @transaction.atomic
    def deleteBuildOutput(self, output):
        """
        Remove a build output from the database:

        - Unallocate any build items against the output
        - Delete the output StockItem
        """

        if not output:
            raise ValidationError(_("No build output specified"))

        if not output.is_building:
            raise ValidationError(_("Build output is already completed"))

        if not output.build == self:
            raise ValidationError(_("Build output does not match Build Order"))

        # Unallocate all build items against the output
        self.unallocateStock(output)

        # Remove the build output from the database
        output.delete()

    @transaction.atomic
    def autoAllocate(self, output):
        """
        Run auto-allocation routine to allocate StockItems to this Build.

        Args:
            output: If specified, only auto-allocate against the given built output

        Returns a list of dict objects with keys like:

            {
                'stock_item': item,
                'quantity': quantity,
            }

        See: getAutoAllocations()
        """

        allocations = self.getAutoAllocations(output)

        for item in allocations:
            # Create a new allocation
            build_item = BuildItem(
                build=self,
                stock_item=item['stock_item'],
                quantity=item['quantity'],
                install_into=output,
            )

            build_item.save()

    @transaction.atomic
    def completeBuildOutput(self, output, user, **kwargs):
        """
        Complete a particular build output

        - Remove allocated StockItems
        - Mark the output as complete
        """

        # Select the location for the build output
        location = kwargs.get('location', self.destination)

        # List the allocated BuildItem objects for the given output
        allocated_items = output.items_to_install.all()

        for build_item in allocated_items:

            # TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
            # TODO: Use celery / redis to offload the actual object deletion...
            # REF: https://www.botreetechnologies.com/blog/implementing-celery-using-django-for-background-task-processing
            # REF: https://code.tutsplus.com/tutorials/using-celery-with-django-for-background-task-processing--cms-28732

            # Complete the allocation of stock for that item
            build_item.complete_allocation(user)

        # Delete the BuildItem objects from the database
        allocated_items.all().delete()

        # Ensure that the output is updated correctly
        output.build = self
        output.is_building = False
        output.location = location

        output.save()

        output.addTransactionNote(_('Completed build output'),
                                  user,
                                  system=True)

        # Increase the completed quantity for this build
        self.completed += output.quantity
        self.save()

    def requiredQuantity(self, part, output):
        """
        Get the quantity of a part required to complete the particular build output.

        Args:
            part: The Part object
            output - The particular build output (StockItem)
        """

        # Extract the BOM line item from the database
        try:
            bom_item = PartModels.BomItem.objects.get(part=self.part.pk,
                                                      sub_part=part.pk)
            quantity = bom_item.quantity
        except (PartModels.BomItem.DoesNotExist):
            quantity = 0

        if output:
            quantity *= output.quantity
        else:
            quantity *= self.remaining

        return quantity

    def allocatedItems(self, part, output):
        """
        Return all BuildItem objects which allocate stock of <part> to <output>

        Args:
            part - The part object
            output - Build output (StockItem).
        """

        allocations = BuildItem.objects.filter(
            build=self,
            stock_item__part=part,
            install_into=output,
        )

        return allocations

    def allocatedQuantity(self, part, output):
        """
        Return the total quantity of given part allocated to a given build output.
        """

        allocations = self.allocatedItems(part, output)

        allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0))

        return allocated['q']

    def unallocatedQuantity(self, part, output):
        """
        Return the total unallocated (remaining) quantity of a part against a particular output.
        """

        required = self.requiredQuantity(part, output)
        allocated = self.allocatedQuantity(part, output)

        return max(required - allocated, 0)

    def isPartFullyAllocated(self, part, output):
        """
        Returns True if the part has been fully allocated to the particular build output
        """

        return self.unallocatedQuantity(part, output) == 0

    def isFullyAllocated(self, output):
        """
        Returns True if the particular build output is fully allocated.
        """

        for bom_item in self.bom_items:
            part = bom_item.sub_part

            if not self.isPartFullyAllocated(part, output):
                return False

        # All parts must be fully allocated!
        return True

    def allocatedParts(self, output):
        """
        Return a list of parts which have been fully allocated against a particular output
        """

        allocated = []

        for bom_item in self.bom_items:
            part = bom_item.sub_part

            if self.isPartFullyAllocated(part, output):
                allocated.append(part)

        return allocated

    def unallocatedParts(self, output):
        """
        Return a list of parts which have *not* been fully allocated against a particular output
        """

        unallocated = []

        for bom_item in self.bom_items:
            part = bom_item.sub_part

            if not self.isPartFullyAllocated(part, output):
                unallocated.append(part)

        return unallocated

    @property
    def required_parts(self):
        """ Returns a dict of parts required to build this part (BOM) """
        parts = []

        for item in self.part.bom_items.all().prefetch_related('sub_part'):
            parts.append(item.sub_part)

        return parts

    def availableStockItems(self, part, output):
        """
        Returns stock items which are available for allocation to this build.

        Args:
            part - Part object
            output - The particular build output
        """

        # Grab initial query for items which are "in stock" and match the part
        items = StockModels.StockItem.objects.filter(
            StockModels.StockItem.IN_STOCK_FILTER)

        items = items.filter(part=part)

        # Exclude any items which have already been allocated
        allocated = BuildItem.objects.filter(
            build=self,
            stock_item__part=part,
            install_into=output,
        )

        items = items.exclude(
            id__in=[item.stock_item.id for item in allocated.all()])

        # Limit query to stock items which are "downstream" of the source location
        if self.take_from is not None:
            items = items.filter(location__in=[
                loc for loc in self.take_from.getUniqueChildren()
            ])

        # Exclude expired stock items
        if not common.models.InvenTreeSetting.get_setting(
                'STOCK_ALLOW_EXPIRED_BUILD'):
            items = items.exclude(StockModels.StockItem.EXPIRED_FILTER)

        return items

    @property
    def is_active(self):
        """ Is this build active? An active build is either:

        - PENDING
        - HOLDING
        """

        return self.status in BuildStatus.ACTIVE_CODES

    @property
    def is_complete(self):
        """ Returns True if the build status is COMPLETE """

        return self.status == BuildStatus.COMPLETE
예제 #2
0
파일: models.py 프로젝트: e-ruiz/brasil.io
class Table(models.Model):
    objects = ActiveTableManager.from_queryset(TableQuerySet)()
    with_hidden = AllTablesManager.from_queryset(TableQuerySet)()

    dataset = models.ForeignKey(Dataset,
                                on_delete=models.CASCADE,
                                null=False,
                                blank=False)
    default = models.BooleanField(null=False, blank=False)
    name = models.CharField(max_length=255, null=False, blank=False)
    options = models.JSONField(null=True, blank=True)
    ordering = ArrayField(models.CharField(max_length=63),
                          null=False,
                          blank=False)
    filtering = ArrayField(models.CharField(max_length=63),
                           null=True,
                           blank=True)
    search = ArrayField(models.CharField(max_length=63), null=True, blank=True)
    version = models.ForeignKey(Version,
                                on_delete=models.CASCADE,
                                null=False,
                                blank=False)
    import_date = models.DateTimeField(null=True, blank=True)
    description = MarkdownxField(null=True, blank=True)
    hidden = models.BooleanField(default=False)
    api_enabled = models.BooleanField(default=True)

    def __str__(self):
        return "{}.{}.{}".format(self.dataset.slug, self.version.name,
                                 self.name)

    @property
    def collect_date(self):
        return self.version.collected_at

    @property
    def data_table(self):
        return self.data_tables.get_current_active()

    @property
    def db_table(self):
        return self.data_table.db_table_name

    @property
    def fields(self):
        return self.field_set.all()

    @property
    def enabled(self):
        return not self.hidden

    @property
    def schema(self):
        db_fields_to_rows_fields = {
            "binary": rows_fields.BinaryField,
            "bool": rows_fields.BoolField,
            "date": rows_fields.DateField,
            "datetime": rows_fields.DatetimeField,
            "decimal": rows_fields.DecimalField,
            "email": rows_fields.EmailField,
            "float": rows_fields.FloatField,
            "integer": rows_fields.IntegerField,
            "json": rows_fields.JSONField,
            "string": rows_fields.TextField,
            "text": rows_fields.TextField,
        }
        return OrderedDict([
            (n, db_fields_to_rows_fields.get(t, rows_fields.Field))
            for n, t in self.fields.values_list("name", "type")
        ])

    @property
    def model_name(self):
        full_name = self.dataset.slug + "-" + self.name
        parts = full_name.replace("_", "-").replace(" ", "-").split("-")
        return "".join([word.capitalize() for word in parts])

    @cached_property
    def dynamic_table_config(self):
        return DynamicTableConfig.get_dynamic_table_customization(
            self.dataset.slug, self.name)

    def get_dynamic_model_managers(self):
        managers = {"objects": DatasetTableModelQuerySet.as_manager()}

        if self.dynamic_table_config:
            managers.update(self.dynamic_table_config.get_model_managers())

        return managers

    def get_dynamic_model_mixins(self):
        mixins = [DatasetTableModelMixin]
        custom_mixins = [] if not self.dynamic_table_config else self.dynamic_table_config.get_model_mixins(
        )
        return custom_mixins + mixins

    def get_model(self, cache=True, data_table=None):
        # TODO: the current dynamic model registry is handled by Brasil.IO's
        # code but it needs to be delegated to dynamic_models.

        data_table = data_table or self.data_table
        db_table = data_table.db_table_name

        # TODO: limit the max number of items in DYNAMIC_MODEL_REGISTRY
        cache_key = (self.id, db_table)
        if cache and cache_key in DYNAMIC_MODEL_REGISTRY:
            return DYNAMIC_MODEL_REGISTRY[cache_key]

        # TODO: unregister the model in Django if already registered (cache_key
        # in DYNAMIC_MODEL_REGISTRY and not cache)
        fields = {field.name: field.field_class for field in self.fields}
        fields["search_data"] = SearchVectorField(null=True)
        ordering = self.ordering or []
        filtering = self.filtering or []
        search = self.search or []
        indexes = []
        # TODO: add has_choices fields also
        if ordering:
            indexes.append(
                django_indexes.Index(
                    name=make_index_name(db_table, "order", ordering),
                    fields=ordering,
                ))
        if filtering:
            for field_name in filtering:
                if ordering == [field_name]:
                    continue
                indexes.append(
                    django_indexes.Index(name=make_index_name(
                        db_table, "filter", [field_name]),
                                         fields=[field_name]))
        if search:
            indexes.append(
                pg_indexes.GinIndex(name=make_index_name(
                    db_table, "search", ["search_data"]),
                                    fields=["search_data"]))

        managers = self.get_dynamic_model_managers()
        mixins = self.get_dynamic_model_mixins()
        meta = {"ordering": ordering, "indexes": indexes, "db_table": db_table}

        Model = dynamic_models.create_model_class(
            name=self.model_name,
            module="core.models",
            fields=fields,
            mixins=mixins,
            meta=meta,
            managers=managers,
        )
        Model.extra = {
            "filtering": filtering,
            "ordering": ordering,
            "search": search,
        }
        DYNAMIC_MODEL_REGISTRY[cache_key] = Model
        return Model

    def get_model_declaration(self):
        Model = self.get_model()
        return dynamic_models.model_source_code(Model)

    def invalidate_cache(self):
        invalidate(self.db_table)
예제 #3
0
class Question(models.Model):
    # Draft->草稿    Publisher -> 已发布
    STATUS = (("O", "Open"), ("C", "Close"), ("D", "Draft"))

    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE,
                             related_name="q_author",
                             verbose_name="提问者")
    title = models.CharField(max_length=255, unique=True, verbose_name="标题")
    slug = models.SlugField(max_length=80,
                            null=True,
                            blank=True,
                            verbose_name='(URL)别名')
    status = models.CharField(max_length=1,
                              choices=STATUS,
                              default="O",
                              verbose_name="问题状态")
    content = MarkdownxField(verbose_name="内容")
    tags = TaggableManager(help_text="多个标签使用,(英文)隔开", verbose_name="标签")
    # 通过GenericRelation关联到Vote表,给问题投票 不是实际的字段
    votes = GenericRelation(Vote, verbose_name="投票情况")
    has_answer = models.BooleanField(default=False, verbose_name="接受回答")

    created_at = models.DateTimeField(db_index=True,
                                      auto_now_add=True,
                                      verbose_name='创建时间')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    objects = QuestionQuerySet.as_manager()

    class Meta:
        verbose_name = "问题"
        verbose_name_plural = verbose_name
        ordering = ('-updated_at', '-created_at')

    def __str__(self):
        return self.title

    def save(self,
             force_insert=False,
             force_update=False,
             using=None,
             update_fields=None):
        if not self.slug:
            self.slug = slugify(self.title)
        super(Question, self).save()

    def get_markdown(self):
        return markdownify(self.content)

    def total_votes(self):
        """总票数"""
        # self.votes.values_list 相当于 Vote.objects.values_list()
        # Counter 分别列出"value" Ture 的数量  与 "value" 为False的数量 的字典
        dic = Counter(self.votes.values_list("value", flat=True))
        return dic[True] - dic[False]

    # 获取所有回答
    def get_answers(self):
        # self为参数表示当前问题有多少回答
        return Answer.objects.filter(question=self).prefetch_related(
            'user', 'question')

    # 回答的数量
    def count_answers(self):
        return self.get_answers().count()

    def get_upvoters(self):
        """赞同用户"""
        # return [vote.user for vote in self.votes.filter(value=True)]
        return [
            vote.user for vote in self.votes.filter(
                value=True).select_related('user').prefetch_related('vote')
        ]

    def get_downvoters(self):
        """踩的用户"""
        # return [vote.user for vote in self.votes.filter(value=False)]
        return [
            vote.user for vote in self.votes.filter(
                value=False).select_related('user').prefetch_related('vote')
        ]

    # 获取问题接受的回答
    def get_accepted_answer(self):
        return Answer.objects.get(question=self, is_answer=True)
예제 #4
0
class MyModel(models.Model):
    myfield = MarkdownxField()
예제 #5
0
class Question(models.Model):
    """Model class to contain every question in the forum."""
    OPEN = "O"
    CLOSED = "C"
    DRAFT = "D"
    STATUS = (
        (OPEN, _("Open")),
        (CLOSED, _("Closed")),
        (DRAFT, _("Draft")),
    )
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=200, unique=True, blank=False)
    timestamp = models.DateTimeField(auto_now_add=True)
    slug = models.SlugField(max_length=80, null=True, blank=True)
    status = models.CharField(max_length=1, choices=STATUS, default=DRAFT)
    content = MarkdownxField()
    has_answer = models.BooleanField(default=False)
    total_votes = models.IntegerField(default=0)
    votes = GenericRelation(Vote)
    tags = TaggableManager()
    objects = QuestionQuerySet.as_manager()

    class Meta:
        ordering = ["-timestamp"]
        verbose_name = _("Question")
        verbose_name_plural = _("Questions")

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(f"{self.title}-{self.id}",
                                to_lower=True, max_length=80)

        super().save(*args, **kwargs)

    def __str__(self):
        return self.title

    @property
    def count_answers(self):
        return Answer.objects.filter(question=self).count()

    def count_votes(self):
        """Method to update the sum of the total votes. Uses this complex query
        to avoid race conditions at database level."""
        dic = Counter(self.votes.values_list("value", flat=True))
        Question.objects.filter(id=self.id).update(total_votes=dic[True] - dic[False])
        self.refresh_from_db()

    def get_upvoters(self):
        """Returns a list containing the users who upvoted the instance."""
        return [vote.user for vote in self.votes.filter(value=True)]

    def get_downvoters(self):
        """Returns a list containing the users who downvoted the instance."""
        return [vote.user for vote in self.votes.filter(value=False)]

    def get_answers(self):
        return Answer.objects.filter(question=self)

    def get_accepted_answer(self):
        return Answer.objects.get(question=self, is_answer=True)

    def get_markdown(self):
        return markdownify(self.content)
예제 #6
0
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
    - A customer is a company to which parts can be sold
    - A manufacturer is a company which manufactures a raw good (they may or may not be a "supplier" also)


    Attributes:
        name: Brief name of the company
        description: Longer form description
        website: URL for the company website
        address: Postal address
        phone: contact phone number
        email: contact email address
        link: Secondary URL e.g. for link to internal Wiki page
        image: Company image / logo
        notes: Extra notes about the company
        is_customer: boolean value, is this company a customer
        is_supplier: boolean value, is this company a supplier
        is_manufacturer: boolean value, is this company a manufacturer
    """

    name = models.CharField(max_length=100,
                            blank=False,
                            unique=True,
                            help_text=_('Company name'),
                            verbose_name=_('Company name'))

    description = models.CharField(max_length=500,
                                   verbose_name=_('Company description'),
                                   help_text=_('Description of the company'))

    website = models.URLField(blank=True,
                              verbose_name=_('Website'),
                              help_text=_('Company website URL'))

    address = models.CharField(max_length=200,
                               verbose_name=_('Address'),
                               blank=True,
                               help_text=_('Company address'))

    phone = models.CharField(max_length=50,
                             verbose_name=_('Phone number'),
                             blank=True,
                             help_text=_('Contact phone number'))

    email = models.EmailField(blank=True,
                              verbose_name=_('Email'),
                              help_text=_('Contact email address'))

    contact = models.CharField(max_length=100,
                               verbose_name=_('Contact'),
                               blank=True,
                               help_text=_('Point of contact'))

    link = InvenTreeURLField(
        blank=True, help_text=_('Link to external company information'))

    image = StdImageField(
        upload_to=rename_company_image,
        null=True,
        blank=True,
        variations={'thumbnail': (128, 128)},
        delete_orphans=True,
    )

    notes = MarkdownxField(blank=True)

    is_customer = models.BooleanField(
        default=False, help_text=_('Do you sell items to this company?'))

    is_supplier = models.BooleanField(
        default=True, help_text=_('Do you purchase items from this company?'))

    is_manufacturer = models.BooleanField(
        default=False, help_text=_('Does this company manufacture parts?'))

    def __str__(self):
        """ Get string representation of a Company """
        return "{n} - {d}".format(n=self.name, d=self.description)

    def get_absolute_url(self):
        """ Get the web URL for the detail view for this Company """
        return reverse('company-detail', kwargs={'pk': self.id})

    def get_image_url(self):
        """ Return the URL of the image for this company """

        if self.image:
            return getMediaUrl(self.image.url)
        else:
            return getBlankImage()

    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:
            return getBlankThumbnail()

    @property
    def manufactured_part_count(self):
        """ The number of parts manufactured by this company """
        return self.manufactured_parts.count()

    @property
    def has_manufactured_parts(self):
        return self.manufactured_part_count > 0

    @property
    def supplied_part_count(self):
        """ The number of parts supplied by this company """
        return self.supplied_parts.count()

    @property
    def has_supplied_parts(self):
        """ Return True if this company supplies any parts """
        return self.supplied_part_count > 0

    @property
    def parts(self):
        """ Return SupplierPart objects which are supplied or manufactured by this company """
        return SupplierPart.objects.filter(
            Q(supplier=self.id) | Q(manufacturer=self.id))

    @property
    def part_count(self):
        """ The number of parts manufactured (or supplied) by this Company """
        return self.parts.count()

    @property
    def has_parts(self):
        return self.part_count > 0

    @property
    def stock_items(self):
        """ Return a list of all stock items supplied or manufactured by this company """
        stock = apps.get_model('stock', 'StockItem')
        return stock.objects.filter(
            Q(supplier_part__supplier=self.id)
            | Q(supplier_part__manufacturer=self.id)).all()

    @property
    def stock_count(self):
        """ Return the number of stock items supplied or manufactured by this company """
        return self.stock_items.count()

    def outstanding_purchase_orders(self):
        """ Return purchase orders which are 'outstanding' """
        return self.purchase_orders.filter(status__in=PurchaseOrderStatus.OPEN)

    def pending_purchase_orders(self):
        """ Return purchase orders which are PENDING (not yet issued) """
        return self.purchase_orders.filter(status=PurchaseOrderStatus.PENDING)

    def closed_purchase_orders(self):
        """ Return purchase orders which are not 'outstanding'

        - Complete
        - Failed / lost
        - Returned
        """

        return self.purchase_orders.exclude(
            status__in=PurchaseOrderStatus.OPEN)

    def complete_purchase_orders(self):
        return self.purchase_orders.filter(status=PurchaseOrderStatus.COMPLETE)

    def failed_purchase_orders(self):
        """ Return any purchase orders which were not successful """

        return self.purchase_orders.filter(
            status__in=PurchaseOrderStatus.FAILED)
예제 #7
0
class Order(MetadataMixin, ReferenceIndexingMixin):
    """ Abstract model for an order.

    Instances of this class:

    - PuchaseOrder

    Attributes:
        reference: Unique order number / reference / code
        description: Long form description (required)
        notes: Extra note field (optional)
        creation_date: Automatic date of order creation
        created_by: User who created this order (automatically captured)
        issue_date: Date the order was issued
        complete_date: Date the order was completed
        responsible: User (or group) responsible for managing the order
    """
    def save(self, *args, **kwargs):

        self.rebuild_reference_field()

        if not self.creation_date:
            self.creation_date = datetime.now().date()

        super().save(*args, **kwargs)

    class Meta:
        abstract = True

    description = models.CharField(max_length=250,
                                   verbose_name=_('Description'),
                                   help_text=_('Order description'))

    link = models.URLField(blank=True,
                           verbose_name=_('Link'),
                           help_text=_('Link to external page'))

    creation_date = models.DateField(blank=True,
                                     null=True,
                                     verbose_name=_('Creation Date'))

    created_by = models.ForeignKey(User,
                                   on_delete=models.SET_NULL,
                                   blank=True,
                                   null=True,
                                   related_name='+',
                                   verbose_name=_('Created By'))

    responsible = models.ForeignKey(
        UserModels.Owner,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        help_text=_('User or group responsible for this order'),
        verbose_name=_('Responsible'),
        related_name='+',
    )

    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.

        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
        price_ref_tag = 'sale_price' if isinstance(
            self, SalesOrder) else 'purchase_price'

        # order items
        for line in self.lines.all():

            price_ref = getattr(line, price_ref_tag)

            if not price_ref:
                continue

            try:
                total += line.quantity * convert_money(price_ref,
                                                       target_currency)
            except MissingRate:
                # Record the error, try to press on
                kind, info, data = sys.exc_info()

                Error.objects.create(
                    kind=kind.__name__,
                    info=info,
                    data='\n'.join(traceback.format_exception(
                        kind, info, data)),
                    path='order.get_total_price',
                )

                logger.error(f"Missing exchange rate for '{target_currency}'")

                # Return None to indicate the calculated price is invalid
                return None

        # extra items
        for line in self.extra_lines.all():

            if not line.price:
                continue

            try:
                total += line.quantity * convert_money(line.price,
                                                       target_currency)
            except MissingRate:
                # Record the error, try to press on
                kind, info, data = sys.exc_info()

                Error.objects.create(
                    kind=kind.__name__,
                    info=info,
                    data='\n'.join(traceback.format_exception(
                        kind, info, data)),
                    path='order.get_total_price',
                )

                logger.error(f"Missing exchange rate for '{target_currency}'")

                # Return None to indicate the calculated price is invalid
                return None

        # set decimal-places
        total.decimal_places = 4

        return total
예제 #8
0
class Dataset(models.Model):
    class Meta:
        db_table = 'Dataset'

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='owner_datasets')
    source_uri = models.CharField(blank=False, null=False, max_length=128)
    source_region = models.CharField(choices=AwsRegionType.choices(),
                                     default=AwsRegionType.eu_west_1,
                                     max_length=128,
                                     help_text='Give the AWS region of the S3 bucket. Required to show the content')
    is_public = models.BooleanField(default=False)
    title = models.CharField(max_length=140, blank=False, null=False, help_text='Give your dataset a descriptive title')
    description = MarkdownxField(help_text='Additional information about your dataset')
    task_type = models.CharField(choices=TaskType.choices(),
                                 default=TaskType.single_image_label,
                                 max_length=128,
                                 help_text='Task Type: label a single image or compare two images')
    labels_per_task = models.PositiveSmallIntegerField(default=1,
                                                       help_text='How many labels should be saved for each task')
    label_names = ArrayField(models.CharField(max_length=128, blank=False),
                             help_text='Give a comma-separated list of the labels in your dataset. Example: "hotdog, not hotdog"')
    keys = ArrayField(models.CharField(max_length=256), null=True, blank=True)
    source_data = JSONField(default=dict,
                            help_text='Keys in your dataset, will be automatically fetched and overwritten each time you save.')
    admins = models.ManyToManyField(User, related_name='admin_datasets', blank=True)
    contributors = models.ManyToManyField(User, related_name='contributor_datasets', blank=True)
    invite_key = models.CharField(max_length=128, null=True)

    def __str__(self):
        return f'{self.title} - {self.source_region}'

    def clean_fields(self, exclude=None):
        if S3_ARN_PREFIX in self.source_uri:
            self.source_uri = self.source_uri.replace(S3_ARN_PREFIX, '')
        try:
            s3 = boto3.resource('s3')
            s3.meta.client.head_bucket(Bucket=self.source_uri)
        except ClientError as e:
            # If a client error is thrown, then check that it was a 404 error.
            # If it was a 404 error, then the bucket does not exist.
            error_code = int(e.response['Error']['Code'])
            if error_code == 403:
                raise ValidationError('Private S3 bucket. Can\'t access data')
            elif error_code == 404:
                raise ValidationError('Could not find S3 bucket %s' % self.source_uri)
        except ParamValidationError as e:
            raise ValidationError('Wrong source bucket name. %s' % e)

    def save(self, *args, **kwargs):
        if not self.invite_key:
            self.invite_key = generate_invite_key(self)

        super(Dataset, self).save(*args, **kwargs)

    def clean_keys_from_source(self):
        bucket_name = get_s3_bucket_from_str(str(self.source_uri))
        s3_bucket = boto3.resource('s3').Bucket(bucket_name)
        removed_keys = []

        for key in self.keys:
            should_delete_key = False
            try:
                file_size = s3_bucket.Object(key).content_length
                if file_size == 0:
                    should_delete_key = True
            except ClientError as e:
                error_code = int(e.response['Error']['Code'])
                if error_code == 404:
                    should_delete_key = True
                else:
                    logger.warning(f'Cleaning dataset {self.id} - received error {e}')

            if should_delete_key:
                self.delete_key_from_dataset(key)
                removed_keys.append(key)

        logger.info(f'Removed {len(removed_keys)} keys')
        self.keys = list(set(self.keys) - set(removed_keys))
        self.save()

    def delete_key_from_dataset(self, key):
        bucket_name = get_s3_bucket_from_str(str(self.source_uri))
        is_deleted = delete_s3_object(bucket_name, key)

        if not is_deleted:
            return 'Could not delete file', None, None

        tasks_with_deleted_file = self.tasks.filter(definition__contains=key)
        task_ids_with_deleted_file = tasks_with_deleted_file.values_list("id", flat=True)
        labels_for_tasks_with_deleted_file = Label.objects.filter(task_id__in=task_ids_with_deleted_file)

        task_count = tasks_with_deleted_file.count()
        label_count = labels_for_tasks_with_deleted_file.count()

        labels_for_tasks_with_deleted_file.delete()
        tasks_with_deleted_file.delete()

        logger.info(f'Removed key {key} in S3, {task_count} tasks and {label_count} labels')
        return None, task_count, label_count

    def fetch_keys_from_source(self):
        bucket_name = get_s3_bucket_from_str(str(self.source_uri))
        s3 = boto3.client('s3')
        paginator = s3.get_paginator('list_objects')
        page_response = paginator.paginate(Bucket=bucket_name)

        keys = []
        logger.info('start to fetch keys from bucket: %s', bucket_name)
        for response in page_response:
            if response['ResponseMetadata']['HTTPStatusCode'] == 200:
                contents = response['Contents']
                keys += list(map(lambda obj: obj['Key'], contents))
                logger.info('received %s keys from bucket %s', len(contents), bucket_name)

        self.keys = keys
        if self.task_type == TaskType.two_image_comparison.value:
            prev_tasks_with_labels = self.tasks.filter(labels__isnull=False)
            prev_task_definitions = set(prev_tasks_with_labels.values_list('definition', flat=True))
            task_definitions = self.get_task_definitions_from_keys(keys=keys,
                                                                   prev_task_definitions=prev_task_definitions)

        else:
            existing_keys = list(self.tasks.values_list('definition', flat=True))
            task_definitions = [key for key in keys if key not in existing_keys]

        logger.info(f'Start to create {len(task_definitions)} new tasks')
        Task.objects.bulk_create([
            Task(dataset=self, definition=definition) for definition in task_definitions
        ])
        logger.info(f'Done creating {len(task_definitions)} tasks')
        self.save()

    @property
    def is_done(self) -> bool:
        return self.labels.count() >= self.nr_required_labels

    @property
    def nr_required_labels(self) -> int:
        return self.tasks.count() * self.labels_per_task

    def is_user_authorised_to_contribute(self, user: User) -> bool:
        if self.is_public:
            return True
        if user.is_anonymous:
            return False

        return self.user == user or user.admin_datasets.filter(id=self.id).exists() \
               or user.contributor_datasets.filter(id=self.id).exists()

    def is_user_authorised_admin(self, user: User) -> bool:
        if user.is_anonymous:
            return False
        else:
            return self.user == user or user.admin_datasets.filter(id=self.id).exists()

    @property
    def users(self):
        return User.objects.filter(Q(contributor_datasets=self) | Q(admin_datasets=self) | Q(owner_datasets=self))

    def get_leaderboard_users(self):
        return self.users.annotate(nr_labels=Count('labels',
                                                   filter=Q(labels__dataset=self,
                                                            labels__action=LabelActionType.solve.value))
                                   ).filter(nr_labels__gt=0).order_by('-nr_labels')

    @property
    def invite_link(self):
        return reverse('signup_with_invite', args=[str(self.invite_key)])

    @staticmethod
    def get_task_definitions_from_keys(keys: List[str],
                                       prev_task_definitions: Collection[str] = None,
                                       ratio: float = 0.01, max_nr_tasks: int = 50000) -> List[str]:
        """
        :param keys: list of keys to be compared to each other
        :param prev_task_definitions: list of previous tasks that should not be removed in new calculation
        :param ratio: ratio of comparison tasks to be opened for each key to all others.
                    1 means every key with all other keys
        :param max_nr_tasks: max number of tasks that can be returned.
                    return can be smaller depending on nr. of keys and ratio
        :return: list of tasks = list of tuples of two keys
        """

        if not prev_task_definitions:
            prev_task_definitions = []

        nr_prev_tasks = len(prev_task_definitions)
        if nr_prev_tasks >= max_nr_tasks:
            raise GenerateComparisonTasksException(
                f'Cannot generate new comparison tasks. {nr_prev_tasks} previous tasks'
                f' is more than {max_nr_tasks} max_nr_tasks')

        if ratio > 1:
            raise GenerateComparisonTasksException(f'Ratio {ratio} is larger 1. Duplicate task creation not supported')

        all_combinations = list(combinations(keys, 2))
        should_ignore_ratio = len(all_combinations) * ratio < len(keys)
        if should_ignore_ratio:
            ratio = 1

        all_combinations = set(all_combinations) - set(prev_task_definitions)
        nr_new_tasks = min(len(all_combinations) * ratio, max_nr_tasks - nr_prev_tasks)
        new_tasks = sample(all_combinations, int(nr_new_tasks))
        tasks_as_str = [f'{k1},{k2}' for k1, k2 in new_tasks]
        return tasks_as_str

    def get_unique_processed_files(self):
        tasks_with_labels = self.tasks.filter(labels__isnull=False)
        if self.task_type == TaskType.two_image_comparison.value:
            annotated_labels = tasks_with_labels.annotate(
                key1=Func(F('definition'), Value(','), Value(1), function='split_part'),
                key2=Func(F('definition'), Value(','), Value(2), function='split_part'))

            unique_keys = set(annotated_labels.values_list('key1', flat=True)).union(
                set(annotated_labels.values_list('key2', flat=True)))
            return unique_keys

        else:
            return tasks_with_labels.all()
예제 #9
0
class Article(models.Model):
    """Represents a piece of news."""

    title = models.CharField('titre',
                             max_length=300,
                             help_text="Titre de l'article")
    slug = models.SlugField(
        max_length=100,
        unique=True,
        help_text=(
            "Un court identifiant généré après la création de l'article."))
    introduction = models.TextField(
        blank=True,
        default='',
        help_text=(
            "Chapeau introductif qui sera affiché sous le titre de l'article. "
            "Utilisez-le pour résumer le contenu de l'article ou introduire "
            "le sujet."))
    content = MarkdownxField(
        'contenu',
        help_text="Contenu complet de l'article (Markdown est supporté).")
    published = models.DateTimeField('publié le', default=now)
    modified = models.DateTimeField('modifié le', auto_now=True)
    image = models.ImageField('illustration',
                              blank=True,
                              null=True,
                              upload_to='articles/')
    display_image = models.BooleanField(
        "afficher l'illustration",
        default=True,
        help_text=(
            "Cocher pour que l'illustration soit affichée sous le chapeau "
            "introductif de l'article."))
    pinned = models.BooleanField(
        'épinglé',
        default=False,
        blank=True,
        help_text=(
            "Cocher pour que l'article soit épinglé et affiché en priorité."))
    # ^blank=True to allow True of False value (otherwise
    # validation would force pinned to be True)
    # see: https://docs.djangoproject.com/fr/2.0/ref/forms/fields/#booleanfield
    categories = models.ManyToManyField(
        'Category',
        blank=True,
        help_text="Catégories auxquelles rattacher l'article",
        verbose_name='catégories')
    active = models.BooleanField(
        'actif',
        default=True,
        blank=True,
        help_text=("Décocher pour que l'article soit archivé. "
                   "Il ne sera alors plus affiché sur le site."))

    def save(self, *args, **kwargs):
        """Assign a slug on article creation."""
        if self.pk is None and not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    class Meta:  # noqa
        verbose_name = 'article'
        ordering = (
            '-active',
            '-pinned',
            '-published',
        )

    def get_absolute_url(self):
        """Return the article's absolute url."""
        return reverse('api:article-detail', args=[str(self.slug)])

    # Permissions

    @staticmethod
    def has_read_permission(request):
        return True

    def has_object_read_permission(self, request):
        return True

    def __str__(self):
        return str(self.title)
예제 #10
0
class Question(models.Model):
    STATUS = (
        ('O', 'Open'),
        ('C', 'Close'),
        ('D', 'Draft'),
    )
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE,
                             related_name='q_author',
                             verbose_name='提问者')
    title = models.CharField(max_length=255, unique=True, verbose_name='标题')
    slug = models.SlugField(max_length=255, verbose_name='(URL)别名')
    status = models.CharField(max_length=1,
                              choices=STATUS,
                              default='O',
                              verbose_name='问题状态')
    content = MarkdownxField(verbose_name='内容')
    tags = TaggableManager(help_text='多个标签用,(英文)隔开', verbose_name='标签')
    has_answer = models.BooleanField(default=False, verbose_name='接受回答')
    votes = GenericRelation(
        Vote, verbose_name='投票情况'
    )  # 通过GenericRelation关联到Vote表定义的GenericForeignKey,vote本身不是实际的字段
    created_at = models.DateTimeField(db_index=True,
                                      auto_now_add=True,
                                      verbose_name='创建时间')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    objects = QuestionQuerrySet.as_manager()

    class Meta:
        verbose_name = '问题'
        verbose_name_plural = verbose_name
        ordering = ('-created_at', )

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super(Question, self).save(*args, **kwargs)

    def get_markdown(self):
        return markdownify(self.content)

    def total_votes(self):
        """得票数"""
        dic = Counter(self.votes.values_list('value', flat=True))
        return dic[True] - dic[False]

    def get_answers(self):
        """所有回答"""
        return Answer.objects.filter(question=self).select_related(
            'user', 'question')

    def count_answers(self):
        """总回答数"""
        return self.get_answers().count()

    def get_upvoters(self):
        """赞成的人"""
        return [
            vote.user for vote in self.votes.filter(
                value=True).select_related('user').prefetch_related('vote')
        ]

    def get_downvoters(self):
        """反对的人"""
        return [
            vote.user for vote in self.votes.filter(
                value=False).select_related('user').prefetch_related('vote')
        ]
예제 #11
0
class CodeOfConduct(SingleActiveModel):
    title = models.CharField(null=True, blank=True, max_length=100)
    description = MarkdownxField(null=True, blank=True)

    def __str__(self):
        return self.title
예제 #12
0
class StockItem(MPTTModel):
    """
    A StockItem object represents a quantity of physical instances of a part.
    
    Attributes:
        parent: Link to another StockItem from which this StockItem was created
        uid: Field containing a unique-id which is mapped to a third-party identifier (e.g. a barcode)
        part: Link to the master abstract part that this StockItem is an instance of
        supplier_part: Link to a specific SupplierPart (optional)
        location: Where this StockItem is located
        quantity: Number of stocked units
        batch: Batch number for this StockItem
        serial: Unique serial number for this StockItem
        link: Optional URL to link to external resource
        updated: Date that this stock item was last updated (auto)
        stocktake_date: Date of last stocktake for this item
        stocktake_user: User that performed the most recent stocktake
        review_needed: Flag if StockItem needs review
        delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero
        status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus)
        notes: Extra notes field
        build: Link to a Build (if this stock item was created from a build)
        purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder)
        infinite: If True this StockItem can never be exhausted
        sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder)
        build_order: Link to a BuildOrder object (if the StockItem has been assigned to a BuildOrder)
    """

    # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
    IN_STOCK_FILTER = Q(
        sales_order=None,
        build_order=None,
        belongs_to=None,
        customer=None,
        status__in=StockStatus.AVAILABLE_CODES
    )

    def save(self, *args, **kwargs):
        """
        Save this StockItem to the database. Performs a number of checks:

        - Unique serial number requirement
        - Adds a transaction note when the item is first created.
        """

        self.validate_unique()
        self.clean()

        if not self.pk:
            # StockItem has not yet been saved
            add_note = True
        else:
            # StockItem has already been saved
            add_note = False

        user = kwargs.pop('user', None)
        
        add_note = add_note and kwargs.pop('note', True)

        super(StockItem, self).save(*args, **kwargs)

        if add_note:
            # This StockItem is being saved for the first time
            self.addTransactionNote(
                'Created stock item',
                user,
                notes="Created new stock item for part '{p}'".format(p=str(self.part)),
                system=True
            )

    @property
    def status_label(self):

        return StockStatus.label(self.status)

    @property
    def serialized(self):
        """ Return True if this StockItem is serialized """
        return self.serial is not None and self.quantity == 1

    def validate_unique(self, exclude=None):
        """
        Test that this StockItem is "unique".
        If the StockItem is serialized, the same serial number.
        cannot exist for the same part (or part tree).
        """

        super(StockItem, self).validate_unique(exclude)

        if self.serial is not None:
            # Query to look for duplicate serial numbers
            parts = PartModels.Part.objects.filter(tree_id=self.part.tree_id)
            stock = StockItem.objects.filter(part__in=parts, serial=self.serial)

            # Exclude myself from the search
            if self.pk is not None:
                stock = stock.exclude(pk=self.pk)

            if stock.exists():
                raise ValidationError({"serial": _("StockItem with this serial number already exists")})

    def clean(self):
        """ Validate the StockItem object (separate to field validation)

        The following validation checks are performed:

        - The 'part' and 'supplier_part.part' fields cannot point to the same Part object
        - The 'part' does not belong to itself
        - Quantity must be 1 if the StockItem has a serial number
        """

        super().clean()

        try:
            if self.part.trackable:
                # Trackable parts must have integer values for quantity field!
                if not self.quantity == int(self.quantity):
                    raise ValidationError({
                        'quantity': _('Quantity must be integer value for trackable parts')
                    })
        except PartModels.Part.DoesNotExist:
            # For some reason the 'clean' process sometimes throws errors because self.part does not exist
            # It *seems* that this only occurs in unit testing, though.
            # Probably should investigate this at some point.
            pass

        if self.quantity < 0:
            raise ValidationError({
                'quantity': _('Quantity must be greater than zero')
            })

        # The 'supplier_part' field must point to the same part!
        try:
            if self.supplier_part is not None:
                if not self.supplier_part.part == self.part:
                    raise ValidationError({'supplier_part': _("Part type ('{pf}') must be {pe}").format(
                                           pf=str(self.supplier_part.part),
                                           pe=str(self.part))
                                           })

            if self.part is not None:
                # A part with a serial number MUST have the quantity set to 1
                if self.serial is not None:
                    if self.quantity > 1:
                        raise ValidationError({
                            'quantity': _('Quantity must be 1 for item with a serial number'),
                            'serial': _('Serial number cannot be set if quantity greater than 1')
                        })

                    if self.quantity == 0:
                        self.quantity = 1

                    elif self.quantity > 1:
                        raise ValidationError({
                            'quantity': _('Quantity must be 1 for item with a serial number')
                        })

                    # Serial numbered items cannot be deleted on depletion
                    self.delete_on_deplete = False

        except PartModels.Part.DoesNotExist:
            # This gets thrown if self.supplier_part is null
            # TODO - Find a test than can be perfomed...
            pass

        if self.belongs_to and self.belongs_to.pk == self.pk:
            raise ValidationError({
                'belongs_to': _('Item cannot belong to itself')
            })

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

    def get_part_name(self):
        return self.part.full_name

    def format_barcode(self, **kwargs):
        """ Return a JSON string for formatting a barcode for this StockItem.
        Can be used to perform lookup of a stockitem using barcode

        Contains the following data:

        { type: 'StockItem', stock_id: <pk>, part_id: <part_pk> }

        Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change)
        """

        return helpers.MakeBarcode(
            "stockitem",
            self.id,
            {
                "url": reverse('api-stock-detail', kwargs={'pk': self.id}),
            },
            **kwargs
        )

    uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))

    parent = TreeForeignKey(
        'self',
        verbose_name=_('Parent Stock Item'),
        on_delete=models.DO_NOTHING,
        blank=True, null=True,
        related_name='children'
    )

    part = models.ForeignKey(
        'part.Part', on_delete=models.CASCADE,
        verbose_name=_('Base Part'),
        related_name='stock_items', help_text=_('Base part'),
        limit_choices_to={
            'active': True,
            'virtual': False
        })

    supplier_part = models.ForeignKey(
        'company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
        verbose_name=_('Supplier Part'),
        help_text=_('Select a matching supplier part for this stock item')
    )

    location = TreeForeignKey(
        StockLocation, on_delete=models.DO_NOTHING,
        verbose_name=_('Stock Location'),
        related_name='stock_items',
        blank=True, null=True,
        help_text=_('Where is this stock item located?')
    )

    belongs_to = models.ForeignKey(
        'self',
        verbose_name=_('Installed In'),
        on_delete=models.DO_NOTHING,
        related_name='owned_parts', blank=True, null=True,
        help_text=_('Is this item installed in another item?')
    )

    customer = models.ForeignKey(
        CompanyModels.Company,
        on_delete=models.SET_NULL,
        null=True, blank=True,
        limit_choices_to={'is_customer': True},
        related_name='assigned_stock',
        help_text=_("Customer"),
        verbose_name=_("Customer"),
    )

    serial = models.CharField(
        verbose_name=_('Serial Number'),
        max_length=100, blank=True, null=True,
        help_text=_('Serial number for this item')
    )
 
    link = InvenTreeURLField(
        verbose_name=_('External Link'),
        max_length=125, blank=True,
        help_text=_("Link to external URL")
    )

    batch = models.CharField(
        verbose_name=_('Batch Code'),
        max_length=100, blank=True, null=True,
        help_text=_('Batch code for this stock item')
    )

    quantity = models.DecimalField(
        verbose_name=_("Stock Quantity"),
        max_digits=15, decimal_places=5, validators=[MinValueValidator(0)],
        default=1
    )

    updated = models.DateField(auto_now=True, null=True)

    build = models.ForeignKey(
        'build.Build', on_delete=models.SET_NULL,
        verbose_name=_('Source Build'),
        blank=True, null=True,
        help_text=_('Build for this stock item'),
        related_name='build_outputs',
    )

    purchase_order = models.ForeignKey(
        'order.PurchaseOrder',
        on_delete=models.SET_NULL,
        verbose_name=_('Source Purchase Order'),
        related_name='stock_items',
        blank=True, null=True,
        help_text=_('Purchase order for this stock item')
    )

    sales_order = models.ForeignKey(
        'order.SalesOrder',
        on_delete=models.SET_NULL,
        verbose_name=_("Destination Sales Order"),
        related_name='stock_items',
        null=True, blank=True)

    build_order = models.ForeignKey(
        'build.Build',
        on_delete=models.SET_NULL,
        verbose_name=_("Destination Build Order"),
        related_name='stock_items',
        null=True, blank=True
    )

    # last time the stock was checked / counted
    stocktake_date = models.DateField(blank=True, null=True)

    stocktake_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
                                       related_name='stocktake_stock')

    review_needed = models.BooleanField(default=False)

    delete_on_deplete = models.BooleanField(default=True, help_text=_('Delete this Stock Item when stock is depleted'))

    status = models.PositiveIntegerField(
        default=StockStatus.OK,
        choices=StockStatus.items(),
        validators=[MinValueValidator(0)])

    notes = MarkdownxField(
        blank=True, null=True,
        verbose_name=_("Notes"),
        help_text=_('Stock Item Notes')
    )

    def clearAllocations(self):
        """
        Clear all order allocations for this StockItem:

        - SalesOrder allocations
        - Build allocations
        """

        # Delete outstanding SalesOrder allocations
        self.sales_order_allocations.all().delete()

        # Delete outstanding BuildOrder allocations
        self.allocations.all().delete()

    def allocateToCustomer(self, customer, quantity=None, order=None, user=None, notes=None):
        """
        Allocate a StockItem to a customer.

        This action can be called by the following processes:
        - Completion of a SalesOrder
        - User manually assigns a StockItem to the customer

        Args:
            customer: The customer (Company) to assign the stock to
            quantity: Quantity to assign (if not supplied, total quantity is used)
            order: SalesOrder reference
            user: User that performed the action
            notes: Notes field
        """

        if quantity is None:
            quantity = self.quantity

        if quantity >= self.quantity:
            item = self
        else:
            item = self.splitStock(quantity, None, user)

        # Update StockItem fields with new information
        item.sales_order = order
        item.customer = customer
        item.location = None

        item.save()

        # TODO - Remove any stock item allocations from this stock item

        item.addTransactionNote(
            _("Assigned to Customer"),
            user,
            notes=_("Manually assigned to customer") + " " + customer.name,
            system=True
        )

        # Return the reference to the stock item
        return item

    def returnFromCustomer(self, location, user=None):
        """
        Return stock item from customer, back into the specified location.
        """

        self.addTransactionNote(
            _("Returned from customer") + " " + self.customer.name,
            user,
            notes=_("Returned to location") + " " + location.name,
            system=True
        )

        self.customer = None
        self.location = location

        self.save()

    # If stock item is incoming, an (optional) ETA field
    # expected_arrival = models.DateField(null=True, blank=True)

    infinite = models.BooleanField(default=False)

    def is_allocated(self):
        """
        Return True if this StockItem is allocated to a SalesOrder or a Build
        """

        # TODO - For now this only checks if the StockItem is allocated to a SalesOrder
        # TODO - In future, once the "build" is working better, check this too

        if self.allocations.count() > 0:
            return True

        if self.sales_order_allocations.count() > 0:
            return True

        return False

    def build_allocation_count(self):
        """
        Return the total quantity allocated to builds
        """

        query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))

        return query['q']

    def sales_order_allocation_count(self):
        """
        Return the total quantity allocated to SalesOrders
        """

        query = self.sales_order_allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))

        return query['q']

    def allocation_count(self):
        """
        Return the total quantity allocated to builds or orders
        """

        return self.build_allocation_count() + self.sales_order_allocation_count()

    def unallocated_quantity(self):
        """
        Return the quantity of this StockItem which is *not* allocated
        """

        return max(self.quantity - self.allocation_count(), 0)

    def can_delete(self):
        """ Can this stock item be deleted? It can NOT be deleted under the following circumstances:

        - Has child StockItems
        - Has a serial number and is tracked
        - Is installed inside another StockItem
        - It has been assigned to a SalesOrder
        - It has been assigned to a BuildOrder
        """

        if self.child_count > 0:
            return False

        if self.part.trackable and self.serial is not None:
            return False

        if self.sales_order is not None:
            return False

        if self.build_order is not None:
            return False

        return True

    @property
    def children(self):
        """ Return a list of the child items which have been split from this stock item """
        return self.get_descendants(include_self=False)

    @property
    def child_count(self):
        """ Return the number of 'child' items associated with this StockItem.
        A child item is one which has been split from this one.
        """
        return self.children.count()

    @property
    def in_stock(self):

        # Not 'in stock' if it has been installed inside another StockItem
        if self.belongs_to is not None:
            return False
            
        # Not 'in stock' if it has been sent to a customer
        if self.sales_order is not None:
            return False

        # Not 'in stock' if it has been allocated to a BuildOrder
        if self.build_order is not None:
            return False

        # Not 'in stock' if it has been assigned to a customer
        if self.customer is not None:
            return False

        # Not 'in stock' if the status code makes it unavailable
        if self.status in StockStatus.UNAVAILABLE_CODES:
            return False

        return True

    @property
    def tracking_info_count(self):
        return self.tracking_info.count()

    @property
    def has_tracking_info(self):
        return self.tracking_info_count > 0

    def addTransactionNote(self, title, user, notes='', url='', system=True):
        """ Generation a stock transaction note for this item.

        Brief automated note detailing a movement or quantity change.
        """
        
        track = StockItemTracking.objects.create(
            item=self,
            title=title,
            user=user,
            quantity=self.quantity,
            date=datetime.now().date(),
            notes=notes,
            link=url,
            system=system
        )

        track.save()

    @transaction.atomic
    def serializeStock(self, quantity, serials, user, notes='', location=None):
        """ Split this stock item into unique serial numbers.

        - Quantity can be less than or equal to the quantity of the stock item
        - Number of serial numbers must match the quantity
        - Provided serial numbers must not already be in use

        Args:
            quantity: Number of items to serialize (integer)
            serials: List of serial numbers (list<int>)
            user: User object associated with action
            notes: Optional notes for tracking
            location: If specified, serialized items will be placed in the given location
        """

        # Cannot serialize stock that is already serialized!
        if self.serialized:
            return

        if not self.part.trackable:
            raise ValidationError({"part": _("Part is not set as trackable")})

        # Quantity must be a valid integer value
        try:
            quantity = int(quantity)
        except ValueError:
            raise ValidationError({"quantity": _("Quantity must be integer")})

        if quantity <= 0:
            raise ValidationError({"quantity": _("Quantity must be greater than zero")})

        if quantity > self.quantity:
            raise ValidationError({"quantity": _("Quantity must not exceed available stock quantity ({n})".format(n=self.quantity))})

        if not type(serials) in [list, tuple]:
            raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")})

        if not quantity == len(serials):
            raise ValidationError({"quantity": _("Quantity does not match serial numbers")})

        # Test if each of the serial numbers are valid
        existing = []

        for serial in serials:
            if self.part.checkIfSerialNumberExists(serial):
                existing.append(serial)

        if len(existing) > 0:
            raise ValidationError({"serial_numbers": _("Serial numbers already exist: ") + str(existing)})

        # Create a new stock item for each unique serial number
        for serial in serials:
            
            # Create a copy of this StockItem
            new_item = StockItem.objects.get(pk=self.pk)
            new_item.quantity = 1
            new_item.serial = serial
            new_item.pk = None
            new_item.parent = self

            if location:
                new_item.location = location

            # The item already has a transaction history, don't create a new note
            new_item.save(user=user, note=False)

            # Copy entire transaction history
            new_item.copyHistoryFrom(self)

            # Copy test result history
            new_item.copyTestResultsFrom(self)

            # Create a new stock tracking item
            new_item.addTransactionNote(_('Add serial number'), user, notes=notes)

        # Remove the equivalent number of items
        self.take_stock(quantity, user, notes=_('Serialized {n} items'.format(n=quantity)))

    @transaction.atomic
    def copyHistoryFrom(self, other):
        """ Copy stock history from another StockItem """

        for item in other.tracking_info.all():
            
            item.item = self
            item.pk = None
            item.save()

    @transaction.atomic
    def copyTestResultsFrom(self, other, filters={}):
        """ Copy all test results from another StockItem """

        for result in other.test_results.all().filter(**filters):

            # Create a copy of the test result by nulling-out the pk
            result.pk = None
            result.stock_item = self
            result.save()

    @transaction.atomic
    def splitStock(self, quantity, location, user):
        """ Split this stock item into two items, in the same location.
        Stock tracking notes for this StockItem will be duplicated,
        and added to the new StockItem.

        Args:
            quantity: Number of stock items to remove from this entity, and pass to the next
            location: Where to move the new StockItem to

        Notes:
            The provided quantity will be subtracted from this item and given to the new one.
            The new item will have a different StockItem ID, while this will remain the same.
        """

        # Do not split a serialized part
        if self.serialized:
            return

        try:
            quantity = Decimal(quantity)
        except (InvalidOperation, ValueError):
            return

        # Doesn't make sense for a zero quantity
        if quantity <= 0:
            return

        # Also doesn't make sense to split the full amount
        if quantity >= self.quantity:
            return

        # Create a new StockItem object, duplicating relevant fields
        # Nullify the PK so a new record is created
        new_stock = StockItem.objects.get(pk=self.pk)
        new_stock.pk = None
        new_stock.parent = self
        new_stock.quantity = quantity

        # Move to the new location if specified, otherwise use current location
        if location:
            new_stock.location = location
        else:
            new_stock.location = self.location

        new_stock.save()

        # Copy the transaction history of this part into the new one
        new_stock.copyHistoryFrom(self)

        # Copy the test results of this part to the new one
        new_stock.copyTestResultsFrom(self)

        # Add a new tracking item for the new stock item
        new_stock.addTransactionNote(
            "Split from existing stock",
            user,
            "Split {n} from existing stock item".format(n=quantity))

        # Remove the specified quantity from THIS stock item
        self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity))

        # Return a copy of the "new" stock item
        return new_stock

    @transaction.atomic
    def move(self, location, notes, user, **kwargs):
        """ Move part to a new location.

        If less than the available quantity is to be moved,
        a new StockItem is created, with the defined quantity,
        and that new StockItem is moved.
        The quantity is also subtracted from the existing StockItem.

        Args:
            location: Destination location (cannot be null)
            notes: User notes
            user: Who is performing the move
            kwargs:
                quantity: If provided, override the quantity (default = total stock quantity)
        """

        try:
            quantity = Decimal(kwargs.get('quantity', self.quantity))
        except InvalidOperation:
            return False

        if not self.in_stock:
            raise ValidationError(_("StockItem cannot be moved as it is not in stock"))

        if quantity <= 0:
            return False

        if location is None:
            # TODO - Raise appropriate error (cannot move to blank location)
            return False
        elif self.location and (location.pk == self.location.pk) and (quantity == self.quantity):
            # TODO - Raise appropriate error (cannot move to same location)
            return False

        # Test for a partial movement
        if quantity < self.quantity:
            # We need to split the stock!

            # Split the existing StockItem in two
            self.splitStock(quantity, location, user)

            return True

        msg = "Moved to {loc}".format(loc=str(location))

        if self.location:
            msg += " (from {loc})".format(loc=str(self.location))

        self.location = location

        self.addTransactionNote(
            msg,
            user,
            notes=notes,
            system=True)

        self.save()

        return True

    @transaction.atomic
    def updateQuantity(self, quantity):
        """ Update stock quantity for this item.
        
        If the quantity has reached zero, this StockItem will be deleted.

        Returns:
            - True if the quantity was saved
            - False if the StockItem was deleted
        """

        # Do not adjust quantity of a serialized part
        if self.serialized:
            return

        try:
            self.quantity = Decimal(quantity)
        except (InvalidOperation, ValueError):
            return

        if quantity < 0:
            quantity = 0

        self.quantity = quantity

        if quantity == 0 and self.delete_on_deplete and self.can_delete():
            
            # TODO - Do not actually "delete" stock at this point - instead give it a "DELETED" flag
            self.delete()
            return False
        else:
            self.save()
            return True

    @transaction.atomic
    def stocktake(self, count, user, notes=''):
        """ Perform item stocktake.
        When the quantity of an item is counted,
        record the date of stocktake
        """

        try:
            count = Decimal(count)
        except InvalidOperation:
            return False

        if count < 0 or self.infinite:
            return False

        self.stocktake_date = datetime.now().date()
        self.stocktake_user = user

        if self.updateQuantity(count):

            self.addTransactionNote('Stocktake - counted {n} items'.format(n=count),
                                    user,
                                    notes=notes,
                                    system=True)

        return True

    @transaction.atomic
    def add_stock(self, quantity, user, notes=''):
        """ Add items to stock
        This function can be called by initiating a ProjectRun,
        or by manually adding the items to the stock location
        """

        # Cannot add items to a serialized part
        if self.serialized:
            return False

        try:
            quantity = Decimal(quantity)
        except InvalidOperation:
            return False

        # Ignore amounts that do not make sense
        if quantity <= 0 or self.infinite:
            return False

        if self.updateQuantity(self.quantity + quantity):
            
            self.addTransactionNote('Added {n} items to stock'.format(n=quantity),
                                    user,
                                    notes=notes,
                                    system=True)

        return True

    @transaction.atomic
    def take_stock(self, quantity, user, notes=''):
        """ Remove items from stock
        """

        # Cannot remove items from a serialized part
        if self.serialized:
            return False

        try:
            quantity = Decimal(quantity)
        except InvalidOperation:
            return False

        if quantity <= 0 or self.infinite:
            return False

        if self.updateQuantity(self.quantity - quantity):

            self.addTransactionNote('Removed {n} items from stock'.format(n=quantity),
                                    user,
                                    notes=notes,
                                    system=True)

        return True

    def __str__(self):
        if self.part.trackable and self.serial:
            s = '{part} #{sn}'.format(
                part=self.part.full_name,
                sn=self.serial)
        else:
            s = '{n} x {part}'.format(
                n=helpers.decimal2string(self.quantity),
                part=self.part.full_name)

        if self.location:
            s += ' @ {loc}'.format(loc=self.location.name)

        return s

    def getTestResults(self, test=None, result=None, user=None):
        """
        Return all test results associated with this StockItem.

        Optionally can filter results by:
        - Test name
        - Test result
        - User
        """

        results = self.test_results

        if test:
            # Filter by test name
            results = results.filter(test=test)

        if result is not None:
            # Filter by test status
            results = results.filter(result=result)

        if user:
            # Filter by user
            results = results.filter(user=user)

        return results

    def testResultMap(self, **kwargs):
        """
        Return a map of test-results using the test name as the key.
        Where multiple test results exist for a given name,
        the *most recent* test is used.

        This map is useful for rendering to a template (e.g. a test report),
        as all named tests are accessible.
        """

        results = self.getTestResults(**kwargs).order_by('-date')

        result_map = {}

        for result in results:
            key = helpers.generateTestKey(result.test)
            result_map[key] = result

        return result_map

    def testResultList(self, **kwargs):
        """
        Return a list of test-result objects for this StockItem
        """

        return self.testResultMap(**kwargs).values()

    def requiredTestStatus(self):
        """
        Return the status of the tests required for this StockItem.

        return:
            A dict containing the following items:
            - total: Number of required tests
            - passed: Number of tests that have passed
            - failed: Number of tests that have failed
        """

        # All the tests required by the part object
        required = self.part.getRequiredTests()

        results = self.testResultMap()

        total = len(required)
        passed = 0
        failed = 0

        for test in required:
            key = helpers.generateTestKey(test.test_name)

            if key in results:
                result = results[key]

                if result.result:
                    passed += 1
                else:
                    failed += 1

        return {
            'total': total,
            'passed': passed,
            'failed': failed,
        }

    @property
    def required_test_count(self):
        return self.part.getRequiredTests().count()

    def hasRequiredTests(self):
        return self.part.getRequiredTests().count() > 0

    def passedAllRequiredTests(self):

        status = self.requiredTestStatus()

        return status['passed'] >= status['total']
예제 #13
0
class Workshop(models.Model):
    title = models.CharField(max_length=300)
    dedicated_qiime2 = models.BooleanField(default=False)
    location = models.CharField(max_length=300)
    description = MarkdownxField()
    email_description = MarkdownxField(help_text='This is the text that is '
                                       'emailed to all workshop attendees '
                                       'when their payment is processed. '
                                       'Supports Markdown.',
                                       blank=True)
    start_date = models.DateField()
    end_date = models.DateField()
    url = models.URLField(verbose_name='URL', max_length=2000, blank=True)
    slug = models.SlugField(help_text='This is the unique identifier for the '
                            'URL (i.e. title-YYYY-MM-DD)')
    draft = models.BooleanField(help_text='Draft workshops do not show up on '
                                'the workshop list overview',
                                default=True)

    @property
    def total_tickets_sold(self):
        return OrderItem.objects.filter(rate__workshop=self) \
                .exclude(order__billed_total='') \
                .exclude(order__refunded=True).count()

    @property
    def is_open(self):
        return self.rate_set.filter(
            private=False, sold_out=False, sales_open=True).count() != 0

    @property
    def available_rates(self):
        return self.rate_set.filter(sold_out=False)

    @property
    def sold_out_rates(self):
        return self.rate_set.filter(sold_out=True)

    class Meta:
        unique_together = (('title', 'slug'), )

    def clean(self):
        # Make sure the workshop begins before it can end...
        if self.start_date > self.end_date:
            raise ValidationError('A Workshop\'s start date must be before '
                                  'the end date.')
        return super().clean()

    def __str__(self):
        return self.title

    # For django admin 'view on site' link
    def get_absolute_url(self):
        return reverse('payments:details',
                       kwargs={'slug': self.slug},
                       subdomain='workshops')

    def filter_rates(self, rate_code):
        rate_set = None
        if rate_code:
            rate_set = self.rate_set.filter(discount_code=rate_code,
                                            sales_open=True).order_by('price')

        if rate_code is None or len(rate_set) == 0:
            rate_set = self.rate_set.filter(private=False,
                                            sales_open=True).order_by('price')
        return rate_set
예제 #14
0
class RequirementModel(models.Model):
    Requirement = MarkdownxField()
예제 #15
0
class SalesOrderShipment(models.Model):
    """
    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
    - When a given SalesOrderShipment is "shipped", stock items are removed from stock

    Attributes:
        order: SalesOrder reference
        shipment_date: Date this shipment was "shipped" (or null)
        checked_by: User reference field indicating who checked this order
        reference: Custom reference text for this shipment (e.g. consignment number?)
        notes: Custom notes field for this shipment
    """
    class Meta:
        # Shipment reference must be unique for a given sales order
        unique_together = [
            'order',
            'reference',
        ]

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

    order = models.ForeignKey(
        SalesOrder,
        on_delete=models.CASCADE,
        blank=False,
        null=False,
        related_name='shipments',
        verbose_name=_('Order'),
        help_text=_('Sales Order'),
    )

    shipment_date = models.DateField(
        null=True,
        blank=True,
        verbose_name=_('Shipment Date'),
        help_text=_('Date of shipment'),
    )

    checked_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        verbose_name=_('Checked By'),
        help_text=_('User who checked this shipment'),
        related_name='+',
    )

    reference = models.CharField(
        max_length=100,
        blank=False,
        verbose_name=('Shipment'),
        help_text=_('Shipment number'),
        default='1',
    )

    notes = MarkdownxField(
        blank=True,
        verbose_name=_('Notes'),
        help_text=_('Shipment notes'),
    )

    tracking_number = models.CharField(
        max_length=100,
        blank=True,
        unique=False,
        verbose_name=_('Tracking Number'),
        help_text=_('Shipment tracking information'),
    )

    invoice_number = models.CharField(
        max_length=100,
        blank=True,
        unique=False,
        verbose_name=_('Invoice Number'),
        help_text=_('Reference number for associated invoice'),
    )

    link = models.URLField(blank=True,
                           verbose_name=_('Link'),
                           help_text=_('Link to external page'))

    def is_complete(self):
        return self.shipment_date is not None

    def check_can_complete(self, raise_error=True):

        try:
            if self.shipment_date:
                # Shipment has already been sent!
                raise ValidationError(_("Shipment has already been sent"))

            if self.allocations.count() == 0:
                raise ValidationError(
                    _("Shipment has no allocated stock items"))

        except ValidationError as e:
            if raise_error:
                raise e
            else:
                return False

        return True

    @transaction.atomic
    def complete_shipment(self, user, **kwargs):
        """
        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()

        allocations = self.allocations.all()

        # Iterate through each stock item assigned to this shipment
        for allocation in allocations:
            # Mark the allocation as "complete"
            allocation.complete_allocation(user)

        # Update the "shipment" date
        self.shipment_date = kwargs.get('shipment_date', datetime.now())
        self.shipped_by = user

        # Was a tracking number provided?
        tracking_number = kwargs.get('tracking_number', None)

        if tracking_number is not None:
            self.tracking_number = tracking_number

        # Was an invoice number provided?
        invoice_number = kwargs.get('invoice_number', None)

        if invoice_number is not None:
            self.invoice_number = invoice_number

        # Was a link provided?
        link = kwargs.get('link', None)

        if link is not None:
            self.link = link

        self.save()

        trigger_event('salesordershipment.completed', id=self.pk)
예제 #16
0
class NewsItem(djm.Model):
    title = djm.CharField(max_length=256)
    summary = djm.TextField(max_length=300)
    content = MarkdownxField()
    datetime = djm.DateTimeField()
    edited = djm.BooleanField(default=False)
    edit_note = djm.CharField(null=True,
                              blank=True,
                              default=None,
                              max_length=256)
    edit_datetime = djm.DateTimeField(null=True, blank=True, default=None)
    author = djm.ForeignKey(settings.AUTH_USER_MODEL,
                            blank=True,
                            null=True,
                            on_delete=djm.SET_NULL,
                            related_name='authored_news_items')
    edit_author = djm.ForeignKey(settings.AUTH_USER_MODEL,
                                 null=True,
                                 blank=True,
                                 on_delete=djm.SET_NULL,
                                 related_name='edited_news_items')
    tags = djm.ManyToManyField(NewsTag, blank=True)
    source = djm.URLField(null=True, blank=True, max_length=256)
    cover_img = djm.ImageField(null=True,
                               blank=True,
                               max_length=256,
                               upload_to=news_item_picture)
    cover_thumbnail = ImageSpecField(
        source='cover_img',
        processors=[SmartResize(*settings.MEDIUM_ICON_SIZE)],
        format='JPEG',
        options={'quality': settings.MEDIUM_QUALITY})
    generated = djm.BooleanField(default=True)

    class Meta:
        unique_together = ['title', 'datetime']
        ordering = ['datetime', 'title']

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('news:item', args=[self.id])

    @property
    def content_html(self):
        return markdownify(self.content)

    def gen_summary(self):
        html = markdownify(self.content)
        soup = BeautifulSoup(html, 'html.parser')
        content = soup.text[:300]
        if len(content) < 300:
            summary = content
        else:
            summary = content.rsplit(' ', 1)[0] + " ..."
        if summary != self.summary:
            self.summary = summary

    @property
    def thumbnail_or_default(self):
        if self.cover_img:
            return self.cover_thumbnail.url
예제 #17
0
class Order(models.Model):
    """ Abstract model for an order.

    Instances of this class:

    - PuchaseOrder

    Attributes:
        reference: Unique order number / reference / code
        description: Long form description (required)
        notes: Extra note field (optional)
        creation_date: Automatic date of order creation
        created_by: User who created this order (automatically captured)
        issue_date: Date the order was issued
        complete_date: Date the order was completed

    """

    ORDER_PREFIX = ""

    @classmethod
    def getNextOrderNumber(cls):
        """
        Try to predict the next order-number
        """

        if cls.objects.count() == 0:
            return None

        # We will assume that the latest pk has the highest PO number
        order = cls.objects.last()
        ref = order.reference

        if not ref:
            return None

        tries = set()

        tries.add(ref)

        while 1:
            new_ref = increment(ref)

            if new_ref in tries:
                # We are in a looping situation - simply return the original one
                return ref

            # Check that the new ref does not exist in the database
            if cls.objects.filter(reference=new_ref).exists():
                tries.add(new_ref)
                new_ref = increment(new_ref)

            else:
                break

        return new_ref

    def __str__(self):
        el = []

        if self.ORDER_PREFIX:
            el.append(self.ORDER_PREFIX)

        el.append(self.reference)

        return " ".join(el)

    def save(self, *args, **kwargs):
        if not self.creation_date:
            self.creation_date = datetime.now().date()

        super().save(*args, **kwargs)

    class Meta:
        abstract = True

    reference = models.CharField(unique=True,
                                 max_length=64,
                                 blank=False,
                                 help_text=_('Order reference'))

    description = models.CharField(max_length=250,
                                   help_text=_('Order description'))

    link = models.URLField(blank=True, help_text=_('Link to external page'))

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

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

    notes = MarkdownxField(blank=True, help_text=_('Order notes'))
예제 #18
0
class Table(models.Model):
    objects = TableQuerySet.as_manager()

    dataset = models.ForeignKey(Dataset,
                                on_delete=models.CASCADE,
                                null=False,
                                blank=False)
    default = models.BooleanField(null=False, blank=False)
    name = models.CharField(max_length=255, null=False, blank=False)
    options = JSONField(null=True, blank=True)
    ordering = ArrayField(models.CharField(max_length=63),
                          null=False,
                          blank=False)
    filtering = ArrayField(models.CharField(max_length=63),
                           null=True,
                           blank=True)
    search = ArrayField(models.CharField(max_length=63), null=True, blank=True)
    version = models.ForeignKey(Version,
                                on_delete=models.CASCADE,
                                null=False,
                                blank=False)
    import_date = models.DateTimeField(null=True, blank=True)
    description = MarkdownxField(null=True, blank=True)

    def __str__(self):
        return "{}.{}.{}".format(self.dataset.slug, self.version.name,
                                 self.name)

    @property
    def db_table(self):
        return "data_{}_{}".format(
            self.dataset.slug.replace("-", ""),
            self.name.replace("_", ""),
        )

    @property
    def fields(self):
        return self.field_set.all()

    @property
    def schema(self):
        db_fields_to_rows_fields = {
            "binary": rows_fields.BinaryField,
            "bool": rows_fields.BoolField,
            "date": rows_fields.DateField,
            "datetime": rows_fields.DatetimeField,
            "decimal": rows_fields.DecimalField,
            "email": rows_fields.EmailField,
            "float": rows_fields.FloatField,
            "integer": rows_fields.IntegerField,
            "json": rows_fields.JSONField,
            "string": rows_fields.TextField,
            "text": rows_fields.TextField,
        }
        return OrderedDict([
            (n, db_fields_to_rows_fields.get(t, rows_fields.Field))
            for n, t in self.fields.values_list("name", "type")
        ])

    def get_model(self, cache=True):
        if cache and self.id in DYNAMIC_MODEL_REGISTRY:
            return DYNAMIC_MODEL_REGISTRY[self.id]

        # TODO: unregister the model in Django if already registered (self.id
        # in DYNAMIC_MODEL_REGISTRY and not cache)
        # TODO: may use Django's internal registry instead of
        # DYNAMIC_MODEL_REGISTRY
        name = self.dataset.slug + "-" + self.name.replace("_", "-")
        model_name = "".join([word.capitalize() for word in name.split("-")])
        fields = {field.name: field.field_class for field in self.fields}
        fields["search_data"] = SearchVectorField(null=True)
        ordering = self.ordering or []
        filtering = self.filtering or []
        search = self.search or []
        indexes = []
        # TODO: add has_choices fields also
        if ordering:
            indexes.append(
                django_indexes.Index(
                    name=make_index_name(name, "order", ordering),
                    fields=ordering,
                ))
        if filtering:
            for field_name in filtering:
                if ordering == [field_name]:
                    continue
                indexes.append(
                    django_indexes.Index(name=make_index_name(
                        name, "filter", [field_name]),
                                         fields=[field_name]))
        if search:
            indexes.append(
                pg_indexes.GinIndex(name=make_index_name(
                    name, "search", ["search_data"]),
                                    fields=["search_data"]))

        Options = type(
            "Meta",
            (object, ),
            {
                "ordering": ordering,
                "indexes": indexes,
                "db_table": self.db_table,
            },
        )
        Model = type(
            model_name,
            (
                DynamicModelMixin,
                models.Model,
            ),
            {
                "__module__": "core.models",
                "Meta": Options,
                "objects": DynamicModelQuerySet.as_manager(),
                **fields,
            },
        )
        Model.extra = {
            "filtering": filtering,
            "ordering": ordering,
            "search": search,
        }
        DYNAMIC_MODEL_REGISTRY[self.id] = Model
        return Model

    def get_model_declaration(self):
        Model = self.get_model()
        return model_to_code(Model)

    def invalidate_cache(self):
        invalidate(self.db_table)
예제 #19
0
class Comment(models.Model):
    post = models.ForeignKey(Post,
                             on_delete=models.CASCADE,
                             related_name='comments',
                             verbose_name='Пост')
    parent = models.ForeignKey('self',
                               on_delete=models.CASCADE,
                               blank=True,
                               null=True,
                               verbose_name='Родитель')
    name = models.CharField(max_length=30, verbose_name='Имя')
    body = MarkdownxField(max_length=3000,
                          blank=False,
                          db_index=False,
                          verbose_name='Текст')
    created_on = models.DateTimeField(auto_now_add=True, verbose_name='Дата')
    mark = models.BooleanField(default=False, verbose_name='Маркер')

    def children(self):  # replies
        return Comment.objects.filter(parent=self)

    @property
    def is_parent(self):
        if self.parent is not None:
            return False
        return True

    @staticmethod
    def current_year():
        return str(datetime.datetime.now().year)

    def formatted_markdown(self):
        return bleach.clean(markdownify(self.body), markdown_tags,
                            markdown_attrs)

    @staticmethod
    def gen_avatar():
        user_avatar_id = randint(1, 16)
        return "{}.jpg".format(user_avatar_id)

    def get_absolute_url(self):
        return reverse('post_detail_url', kwargs={
            'slug': self.post.slug
        }) + "#comment{}".format(self.id)

    def get_delete_url(self):
        return reverse('comment_delete_url',
                       kwargs={
                           'slug': self.post.slug,
                           'id': self.id
                       })

    def get_api_url(self):
        return 'https://warkentin.ru' + reverse(
            'post_detail_api_url', kwargs={'slug': self.post.slug})

    class Meta:
        ordering = ['created_on']

    def __str__(self):
        return '{} пишет: «{}»'.format(self.name, self.body)
예제 #20
0
파일: user.py 프로젝트: arbuz-team/medinox
class Model_Markdown(models.Model):

    markdown = MarkdownxField()
예제 #21
0
class Post(models.Model):
    title = models.CharField(max_length=150, verbose_name='Заголовок')
    slug = models.SlugField(max_length=150,
                            blank=True,
                            unique=True,
                            verbose_name='УРЛ')
    body = MarkdownxField(blank=True, db_index=False, verbose_name='Текст')
    tags = models.ManyToManyField('Tag',
                                  blank=True,
                                  related_name='posts',
                                  verbose_name='Теги')
    reading_time = models.PositiveSmallIntegerField(
        default=0, verbose_name='Время чтения')
    og_image = models.URLField(
        default=
        "https://res.cloudinary.com/wark/image/upload/v1582458310/og_default.jpg",
        verbose_name='Картинка для соцсетей')
    allow_comments = models.BooleanField(default=True,
                                         verbose_name='Открытые комментарии')
    date_pub = models.DateTimeField(auto_now_add=True)

    def formatted_markdown(self):
        return markdownify(self.body)

    def get_absolute_url(self):
        return reverse('post_detail_url', kwargs={'slug': self.slug})

    def get_update_url(self):
        return reverse('post_update_url', kwargs={'slug': self.slug})

    def get_delete_url(self):
        return reverse('post_delete_url', kwargs={'slug': self.slug})

    def get_comments_without_replies(self):
        return Comment.objects.filter(post_id=self.id, parent_id=None)

    def get_replies(self):
        return Comment.objects.filter(post_id=self.id).exclude(parent_id=None)

    def get_reading_time(self):
        # convert markdown text to html code
        article_html = markdownify(self.body)

        # count words
        world_string = strip_tags(article_html)
        matching_words = re.findall(r'\w+', world_string)
        count = len(matching_words)

        # reading time
        # assuming 200wpm reading
        reading_time_min = math.ceil(count / 200.0)
        return int(reading_time_min)

    def save(self, *args, **kwargs):
        if self.body:
            self.reading_time = self.get_reading_time()

        if not self.slug:
            new_slug = slugify(self.title, allow_unicode=True)
            self.slug = new_slug + '-' + str(
                int(datetime.datetime.now().microsecond))
        super().save(*args, **kwargs)

    @staticmethod
    def current_year():
        return str(datetime.datetime.now().year)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-date_pub']
예제 #22
0
class Answer(models.Model):
    uuid_id = models.UUIDField(primary_key=True,
                               default=uuid.uuid4,
                               editable=False)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE,
                             related_name="a_author",
                             verbose_name="问题回答者")
    question = models.ForeignKey(Question,
                                 on_delete=models.CASCADE,
                                 verbose_name="")
    content = MarkdownxField(verbose_name="回答的特容")
    is_answer = models.BooleanField(default=False, verbose_name="回答是否呗接受")
    votes = GenericRelation(Vote, verbose_name='投票情况')
    created_at = models.DateTimeField(db_index=True,
                                      auto_now_add=True,
                                      verbose_name='创建时间')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')

    class Meta:
        verbose_name = "回答"
        verbose_name_plural = verbose_name
        # 多字段排序
        ordering = ('-is_answer', '-created_at')

    def __str__(self):
        return self.content

    def get_markdown(self):
        return markdownify(self.content)

    def total_votes(self):
        """总票数"""
        # self.votes.value_list 相当于 Vote.objects.value_list()
        dic = Counter(Vote.objects.values_list("value", flat=True))
        return dic[True] - dic[False]

    def get_upvoters(self):
        """赞同用户"""
        # return [vote.user for vote in self.votes.filter(value=True)]
        return [
            vote.user for vote in self.votes.filter(
                value=True).select_related('user').prefetch_related('vote')
        ]

    def get_downvoters(self):
        """踩的用户"""
        # return [vote.user for vote in self.votes.filter(value=False)]
        return [
            vote.user for vote in self.votes.filter(
                value=False).select_related('user').prefetch_related('vote')
        ]

    # 获取问题接受的回答
    def get_accepted_answer(self):
        return Answer.objects.get(question=self, is_answer=True)

    # 采纳回答
    def accept_answer(self):
        # 当一个问题有多个回答时,只接受一个答案,其他答案不接受
        answer_set = Answer.objects.filter(question=self.question)  # 查询当前所有答案
        answer_set.update(is_answer=False)  # 一律设为未接受
        # 接受当前回答并保存
        self.is_answer = True
        self.save()
        # 该问题已有呗接受的答案
        self.question.has_answer = True
        self.question.save()
예제 #23
0
class PackageItem(ActiveModel):
    description = MarkdownxField()

    def __str__(self):
        return self.description[0:100]
예제 #24
0
class Part(models.Model):
    """ The Part object represents an abstract part, the 'concept' of an actual entity.

    An actual physical instance of a Part is a StockItem which is treated separately.

    Parts can be used to create other parts (as part of a Bill of Materials or BOM).

    Attributes:
        name: Brief name for this part
        variant: Optional variant number for this part - Must be unique for the part name
        category: The PartCategory to which this part belongs
        description: Longer form description of the part
        keywords: Optional keywords for improving part search results
        IPN: Internal part number (optional)
        revision: Part revision
        is_template: If True, this part is a 'template' part and cannot be instantiated as a StockItem
        link: Link to an external page with more information about this part (e.g. internal Wiki)
        image: Image of this part
        default_location: Where the item is normally stored (may be null)
        default_supplier: The default SupplierPart which should be used to procure and stock this part
        minimum_stock: Minimum preferred quantity to keep in stock
        units: Units of measure for this part (default='pcs')
        salable: Can this part be sold to customers?
        assembly: Can this part be build from other parts?
        component: Can this part be used to make other parts?
        purchaseable: Can this part be purchased from suppliers?
        trackable: Trackable parts can have unique serial numbers assigned, etc, etc
        active: Is this part active? Parts are deactivated instead of being deleted
        virtual: Is this part "virtual"? e.g. a software product or similar
        notes: Additional notes field for this part
        creation_date: Date that this part was added to the database
        creation_user: User who added this part to the database
        responsible: User who is responsible for this part (optional)
    """
    class Meta:
        verbose_name = "Part"
        verbose_name_plural = "Parts"

    def save(self, *args, **kwargs):
        """
        Overrides the save() function for the Part model.
        If the part image has been updated,
        then check if the "old" (previous) image is still used by another part.
        If not, it is considered "orphaned" and will be deleted.
        """

        if self.pk:
            previous = Part.objects.get(pk=self.pk)

            if previous.image and not self.image == previous.image:
                # Are there any (other) parts which reference the image?
                n_refs = Part.objects.filter(image=previous.image).exclude(
                    pk=self.pk).count()

                if n_refs == 0:
                    previous.image.delete(save=False)

        super().save(*args, **kwargs)

    def __str__(self):
        return "{n} - {d}".format(n=self.full_name, d=self.description)

    @property
    def full_name(self):
        """ Format a 'full name' for this Part.

        - IPN (if not null)
        - Part name
        - Part variant (if not null)

        Elements are joined by the | character
        """

        elements = []

        if self.IPN:
            elements.append(self.IPN)

        elements.append(self.name)

        if self.revision:
            elements.append(self.revision)

        return ' | '.join(elements)

    def set_category(self, category):

        # Ignore if the category is already the same
        if self.category == category:
            return

        self.category = category
        self.save()

    def get_absolute_url(self):
        """ Return the web URL for viewing this part """
        return reverse('part-detail', kwargs={'pk': self.id})

    def get_image_url(self):
        """ Return the URL of the image for this part """

        if self.image:
            return helpers.getMediaUrl(self.image.url)
        else:
            return helpers.getBlankImage()

    def get_thumbnail_url(self):
        """
        Return the URL of the image thumbnail for this part
        """

        if self.image:
            return helpers.getMediaUrl(self.image.thumbnail.url)
        else:
            return helpers.getBlankThumbnail()

    def validate_unique(self, exclude=None):
        """ Validate that a part is 'unique'.
        Uniqueness is checked across the following (case insensitive) fields:

        * Name
        * IPN
        * Revision

        e.g. there can exist multiple parts with the same name, but only if
        they have a different revision or internal part number.

        """
        super().validate_unique(exclude)

        # Part name uniqueness should be case insensitive
        try:
            parts = Part.objects.exclude(id=self.id).filter(
                name__iexact=self.name,
                IPN__iexact=self.IPN,
                revision__iexact=self.revision)

            if parts.exists():
                msg = _("Part must be unique for name, IPN and revision")
                raise ValidationError({
                    "name": msg,
                    "IPN": msg,
                    "revision": msg,
                })
        except Part.DoesNotExist:
            pass

    def clean(self):
        """ Perform cleaning operations for the Part model """

        if self.is_template and self.variant_of is not None:
            raise ValidationError({
                'is_template':
                _("Part cannot be a template part if it is a variant of another part"
                  ),
                'variant_of':
                _("Part cannot be a variant of another part if it is already a template"
                  ),
            })

    name = models.CharField(max_length=100,
                            blank=False,
                            help_text=_('Part name'),
                            validators=[validators.validate_part_name])

    is_template = models.BooleanField(
        default=False, help_text=_('Is this part a template part?'))

    variant_of = models.ForeignKey(
        'part.Part',
        related_name='variants',
        null=True,
        blank=True,
        limit_choices_to={
            'is_template': True,
            'active': True,
        },
        on_delete=models.SET_NULL,
        help_text=_('Is this part a variant of another part?'))

    description = models.CharField(max_length=250,
                                   blank=False,
                                   help_text=_('Part description'))

    keywords = models.CharField(
        max_length=250,
        blank=True,
        help_text=_('Part keywords to improve visibility in search results'))

    category = TreeForeignKey(PartCategory,
                              related_name='parts',
                              null=True,
                              blank=True,
                              on_delete=models.DO_NOTHING,
                              help_text=_('Part category'))

    IPN = models.CharField(max_length=100,
                           blank=True,
                           help_text=_('Internal Part Number'),
                           validators=[validators.validate_part_ipn])

    revision = models.CharField(max_length=100,
                                blank=True,
                                help_text=_('Part revision or version number'))

    link = InvenTreeURLField(blank=True, help_text=_('Link to extenal URL'))

    image = StdImageField(
        upload_to=rename_part_image,
        null=True,
        blank=True,
        variations={'thumbnail': (128, 128)},
        delete_orphans=True,
    )

    default_location = TreeForeignKey(
        'stock.StockLocation',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        help_text=_('Where is this item normally stored?'),
        related_name='default_parts')

    def get_default_location(self):
        """ Get the default location for a Part (may be None).

        If the Part does not specify a default location,
        look at the Category this part is in.
        The PartCategory object may also specify a default stock location
        """

        if self.default_location:
            return self.default_location
        elif self.category:
            # Traverse up the category tree until we find a default location
            cats = self.category.get_ancestors(ascending=True,
                                               include_self=True)

            for cat in cats:
                if cat.default_location:
                    return cat.default_location

        # Default case - no default category found
        return None

    def get_default_supplier(self):
        """ Get the default supplier part for this part (may be None).

        - If the part specifies a default_supplier, return that
        - If there is only one supplier part available, return that
        - Else, return None
        """

        if self.default_supplier:
            return self.default_supplier

        if self.supplier_count == 1:
            return self.supplier_parts.first()

        # Default to None if there are multiple suppliers to choose from
        return None

    default_supplier = models.ForeignKey(SupplierPart,
                                         on_delete=models.SET_NULL,
                                         blank=True,
                                         null=True,
                                         help_text=_('Default supplier part'),
                                         related_name='default_parts')

    minimum_stock = models.PositiveIntegerField(
        default=0,
        validators=[MinValueValidator(0)],
        help_text=_('Minimum allowed stock level'))

    units = models.CharField(max_length=20,
                             default="",
                             blank=True,
                             help_text=_('Stock keeping units for this part'))

    assembly = models.BooleanField(
        default=False,
        verbose_name='Assembly',
        help_text=_('Can this part be built from other parts?'))

    component = models.BooleanField(
        default=True,
        verbose_name='Component',
        help_text=_('Can this part be used to build other parts?'))

    trackable = models.BooleanField(
        default=False,
        help_text=_('Does this part have tracking for unique items?'))

    purchaseable = models.BooleanField(
        default=True,
        help_text=_('Can this part be purchased from external suppliers?'))

    salable = models.BooleanField(
        default=False, help_text=_("Can this part be sold to customers?"))

    active = models.BooleanField(default=True,
                                 help_text=_('Is this part active?'))

    virtual = models.BooleanField(
        default=False,
        help_text=_(
            'Is this a virtual part, such as a software product or license?'))

    notes = MarkdownxField(
        blank=True, help_text=_('Part notes - supports Markdown formatting'))

    bom_checksum = models.CharField(max_length=128,
                                    blank=True,
                                    help_text=_('Stored BOM checksum'))

    bom_checked_by = models.ForeignKey(User,
                                       on_delete=models.SET_NULL,
                                       blank=True,
                                       null=True,
                                       related_name='boms_checked')

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

    creation_date = models.DateField(auto_now_add=True,
                                     editable=False,
                                     blank=True,
                                     null=True)

    creation_user = models.ForeignKey(User,
                                      on_delete=models.SET_NULL,
                                      blank=True,
                                      null=True,
                                      related_name='parts_created')

    responsible = models.ForeignKey(User,
                                    on_delete=models.SET_NULL,
                                    blank=True,
                                    null=True,
                                    related_name='parts_responible')

    def format_barcode(self):
        """ Return a JSON string for formatting a barcode for this Part object """

        return helpers.MakeBarcode(
            "part", {
                "id": self.id,
                "name": self.full_name,
                "url": reverse('api-part-detail', kwargs={'pk': self.id}),
            })

    @property
    def category_path(self):
        if self.category:
            return self.category.pathstring
        return ''

    @property
    def available_stock(self):
        """
        Return the total available stock.

        - This subtracts stock which is already allocated to builds
        """

        total = self.total_stock
        total -= self.allocation_count()

        return max(total, 0)

    @property
    def quantity_to_order(self):
        """ Return the quantity needing to be ordered for this part. """

        required = -1 * self.net_stock
        return max(required, 0)

    @property
    def net_stock(self):
        """ Return the 'net' stock. It takes into account:

        - Stock on hand (total_stock)
        - Stock on order (on_order)
        - Stock allocated (allocation_count)

        This number (unlike 'available_stock') can be negative.
        """

        return self.total_stock - self.allocation_count() + self.on_order

    def isStarredBy(self, user):
        """ Return True if this part has been starred by a particular user """

        try:
            PartStar.objects.get(part=self, user=user)
            return True
        except PartStar.DoesNotExist:
            return False

    def need_to_restock(self):
        """ Return True if this part needs to be restocked
        (either by purchasing or building).

        If the allocated_stock exceeds the total_stock,
        then we need to restock.
        """

        return (self.total_stock + self.on_order -
                self.allocation_count) < self.minimum_stock

    @property
    def can_build(self):
        """ Return the number of units that can be build with available stock
        """

        # If this part does NOT have a BOM, result is simply the currently available stock
        if not self.has_bom:
            return 0

        total = None

        # Calculate the minimum number of parts that can be built using each sub-part
        for item in self.bom_items.all().prefetch_related(
                'sub_part__stock_items'):
            stock = item.sub_part.available_stock
            n = int(stock / item.quantity)

            if total is None or n < total:
                total = n

        return max(total, 0)

    @property
    def active_builds(self):
        """ Return a list of outstanding builds.
        Builds marked as 'complete' or 'cancelled' are ignored
        """

        return self.builds.filter(status__in=BuildStatus.ACTIVE_CODES)

    @property
    def inactive_builds(self):
        """ Return a list of inactive builds
        """

        return self.builds.exclude(status__in=BuildStatus.ACTIVE_CODES)

    @property
    def quantity_being_built(self):
        """ Return the current number of parts currently being built
        """

        quantity = self.active_builds.aggregate(
            quantity=Sum('quantity'))['quantity']

        if quantity is None:
            quantity = 0

        return quantity

    def build_order_allocations(self):
        """
        Return all 'BuildItem' objects which allocate this part to Build objects
        """

        return BuildModels.BuildItem.objects.filter(
            stock_item__part__id=self.id)

    def build_order_allocation_count(self):
        """
        Return the total amount of this part allocated to build orders
        """

        query = self.build_order_allocations().aggregate(
            total=Coalesce(Sum('quantity'), 0))

        return query['total']

    def sales_order_allocations(self):
        """
        Return all sales-order-allocation objects which allocate this part to a SalesOrder
        """

        return OrderModels.SalesOrderAllocation.objects.filter(
            item__part__id=self.id)

    def sales_order_allocation_count(self):
        """
        Return the tutal quantity of this part allocated to sales orders
        """

        query = self.sales_order_allocations().aggregate(
            total=Coalesce(Sum('quantity'), 0))

        return query['total']

    def allocation_count(self):
        """
        Return the total quantity of stock allocated for this part,
        against both build orders and sales orders.
        """

        return sum([
            self.build_order_allocation_count(),
            self.sales_order_allocation_count(),
        ])

    @property
    def stock_entries(self):
        """ Return all 'in stock' items. To be in stock:

        - build_order is None
        - sales_order is None
        - belongs_to is None
        """

        return self.stock_items.filter(StockModels.StockItem.IN_STOCK_FILTER)

    @property
    def total_stock(self):
        """ Return the total stock quantity for this part.
        Part may be stored in multiple locations
        """

        if self.is_template:
            total = sum(
                [variant.total_stock for variant in self.variants.all()])
        else:
            total = self.stock_entries.filter(
                status__in=StockStatus.AVAILABLE_CODES).aggregate(
                    total=Sum('quantity'))['total']

        if total:
            return total
        else:
            return Decimal(0)

    @property
    def has_bom(self):
        return self.bom_count > 0

    @property
    def bom_count(self):
        """ Return the number of items contained in the BOM for this part """
        return self.bom_items.count()

    @property
    def used_in_count(self):
        """ Return the number of part BOMs that this part appears in """
        return self.used_in.count()

    def get_bom_hash(self):
        """ Return a checksum hash for the BOM for this part.
        Used to determine if the BOM has changed (and needs to be signed off!)

        The hash is calculated by hashing each line item in the BOM.

        returns a string representation of a hash object which can be compared with a stored value
        """

        hash = hashlib.md5(str(self.id).encode())

        for item in self.bom_items.all().prefetch_related('sub_part'):
            hash.update(str(item.get_item_hash()).encode())

        return str(hash.digest())

    @property
    def is_bom_valid(self):
        """ Check if the BOM is 'valid' - if the calculated checksum matches the stored value
        """

        return self.get_bom_hash() == self.bom_checksum

    @transaction.atomic
    def validate_bom(self, user):
        """ Validate the BOM (mark the BOM as validated by the given User.

        - Calculates and stores the hash for the BOM
        - Saves the current date and the checking user
        """

        # Validate each line item too
        for item in self.bom_items.all():
            item.validate_hash()

        self.bom_checksum = self.get_bom_hash()
        self.bom_checked_by = user
        self.bom_checked_date = datetime.now().date()

        self.save()

    @transaction.atomic
    def clear_bom(self):
        """ Clear the BOM items for the part (delete all BOM lines).
        """

        self.bom_items.all().delete()

    def required_parts(self):
        """ Return a list of parts required to make this part (list of BOM items) """
        parts = []
        for bom in self.bom_items.all().select_related('sub_part'):
            parts.append(bom.sub_part)
        return parts

    def get_allowed_bom_items(self):
        """ Return a list of parts which can be added to a BOM for this part.

        - Exclude parts which are not 'component' parts
        - Exclude parts which this part is in the BOM for
        """

        parts = Part.objects.filter(component=True).exclude(id=self.id)
        parts = parts.exclude(id__in=[part.id for part in self.used_in.all()])

        return parts

    @property
    def supplier_count(self):
        """ Return the number of supplier parts available for this part """
        return self.supplier_parts.count()

    @property
    def has_pricing_info(self):
        """ Return true if there is pricing information for this part """
        return self.get_price_range() is not None

    @property
    def has_complete_bom_pricing(self):
        """ Return true if there is pricing information for each item in the BOM. """

        for item in self.bom_items.all().select_related('sub_part'):
            if not item.sub_part.has_pricing_info:
                return False

        return True

    def get_price_info(self, quantity=1, buy=True, bom=True):
        """ Return a simplified pricing string for this part
        
        Args:
            quantity: Number of units to calculate price for
            buy: Include supplier pricing (default = True)
            bom: Include BOM pricing (default = True)
        """

        price_range = self.get_price_range(quantity, buy, bom)

        if price_range is None:
            return None

        min_price, max_price = price_range

        if min_price == max_price:
            return min_price

        min_price = normalize(min_price)
        max_price = normalize(max_price)

        return "{a} - {b}".format(a=min_price, b=max_price)

    def get_supplier_price_range(self, quantity=1):

        min_price = None
        max_price = None

        for supplier in self.supplier_parts.all():

            price = supplier.get_price(quantity)

            if price is None:
                continue

            if min_price is None or price < min_price:
                min_price = price

            if max_price is None or price > max_price:
                max_price = price

        if min_price is None or max_price is None:
            return None

        min_price = normalize(min_price)
        max_price = normalize(max_price)

        return (min_price, max_price)

    def get_bom_price_range(self, quantity=1):
        """ Return the price range of the BOM for this part.
        Adds the minimum price for all components in the BOM.

        Note: If the BOM contains items without pricing information,
        these items cannot be included in the BOM!
        """

        min_price = None
        max_price = None

        for item in self.bom_items.all().select_related('sub_part'):

            if item.sub_part.pk == self.pk:
                print("Warning: Item contains itself in BOM")
                continue

            prices = item.sub_part.get_price_range(quantity * item.quantity)

            if prices is None:
                continue

            low, high = prices

            if min_price is None:
                min_price = 0

            if max_price is None:
                max_price = 0

            min_price += low
            max_price += high

        if min_price is None or max_price is None:
            return None

        min_price = normalize(min_price)
        max_price = normalize(max_price)

        return (min_price, max_price)

    def get_price_range(self, quantity=1, buy=True, bom=True):
        """ Return the price range for this part. This price can be either:

        - Supplier price (if purchased from suppliers)
        - BOM price (if built from other parts)

        Returns:
            Minimum of the supplier price or BOM price. If no pricing available, returns None
        """

        buy_price_range = self.get_supplier_price_range(
            quantity) if buy else None
        bom_price_range = self.get_bom_price_range(quantity) if bom else None

        if buy_price_range is None:
            return bom_price_range

        elif bom_price_range is None:
            return buy_price_range

        else:
            return (min(buy_price_range[0], bom_price_range[0]),
                    max(buy_price_range[1], bom_price_range[1]))

    def deepCopy(self, other, **kwargs):
        """ Duplicates non-field data from another part.
        Does not alter the normal fields of this part,
        but can be used to copy other data linked by ForeignKey refernce.

        Keyword Args:
            image: If True, copies Part image (default = True)
            bom: If True, copies BOM data (default = False)
        """

        # Copy the part image
        if kwargs.get('image', True):
            if other.image:
                # Reference the other image from this Part
                self.image = other.image

        # Copy the BOM data
        if kwargs.get('bom', False):
            for item in other.bom_items.all():
                # Point the item to THIS part.
                # Set the pk to None so a new entry is created.
                item.part = self
                item.pk = None
                item.save()

        # Copy the fields that aren't available in the duplicate form
        self.salable = other.salable
        self.assembly = other.assembly
        self.component = other.component
        self.purchaseable = other.purchaseable
        self.trackable = other.trackable
        self.virtual = other.virtual

        self.save()

    @property
    def attachment_count(self):
        """ Count the number of attachments for this part.
        If the part is a variant of a template part,
        include the number of attachments for the template part.

        """

        n = self.attachments.count()

        if self.variant_of:
            n += self.variant_of.attachments.count()

        return n

    def sales_orders(self):
        """ Return a list of sales orders which reference this part """

        orders = []

        for line in self.sales_order_line_items.all().prefetch_related(
                'order'):
            if line.order not in orders:
                orders.append(line.order)

        return orders

    def purchase_orders(self):
        """ Return a list of purchase orders which reference this part """

        orders = []

        for part in self.supplier_parts.all().prefetch_related(
                'purchase_order_line_items'):
            for order in part.purchase_orders():
                if order not in orders:
                    orders.append(order)

        return orders

    def open_purchase_orders(self):
        """ Return a list of open purchase orders against this part """

        return [
            order for order in self.purchase_orders()
            if order.status in PurchaseOrderStatus.OPEN
        ]

    def closed_purchase_orders(self):
        """ Return a list of closed purchase orders against this part """

        return [
            order for order in self.purchase_orders()
            if order.status not in PurchaseOrderStatus.OPEN
        ]

    @property
    def on_order(self):
        """ Return the total number of items on order for this part. """

        orders = self.supplier_parts.filter(
            purchase_order_line_items__order__status__in=PurchaseOrderStatus.
            OPEN).aggregate(
                quantity=Sum('purchase_order_line_items__quantity'),
                received=Sum('purchase_order_line_items__received'))

        quantity = orders['quantity']
        received = orders['received']

        if quantity is None:
            quantity = 0

        if received is None:
            received = 0

        return quantity - received

    def get_parameters(self):
        """ Return all parameters for this part, ordered by name """

        return self.parameters.order_by('template__name')
예제 #25
0
파일: models.py 프로젝트: donaldchi/MyBlog
class Tag(models.Model):
    name = models.CharField(max_length=255)
    description = MarkdownxField()

    def __str__(self):
        return self.name
예제 #26
0
class Build(MPTTModel):
    """ A Build object organises the creation of new parts from the component parts.

    Attributes:
        part: The part to be built (from component BOM items)
        title: Brief title describing the build (required)
        quantity: Number of units to be built
        parent: Reference to a Build object for which this Build is required
        sales_order: References to a SalesOrder object for which this Build is required (e.g. the output of this build will be used to fulfil a sales order)
        take_from: Location to take stock from to make this build (if blank, can take from anywhere)
        status: Build status code
        batch: Batch code transferred to build parts (optional)
        creation_date: Date the build was created (auto)
        completion_date: Date the build was completed
        link: External URL for extra information
        notes: Text notes
    """

    def __str__(self):
        return "{q} x {part}".format(q=decimal2string(self.quantity), part=str(self.part.full_name))

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

    title = models.CharField(
        verbose_name=_('Build Title'),
        blank=False,
        max_length=100,
        help_text=_('Brief description of the build')
    )

    parent = TreeForeignKey(
        'self',
        on_delete=models.DO_NOTHING,
        blank=True, null=True,
        related_name='children',
        verbose_name=_('Parent Build'),
        help_text=_('Parent build to which this build is allocated'),
    )

    part = models.ForeignKey(
        'part.Part',
        verbose_name=_('Part'),
        on_delete=models.CASCADE,
        related_name='builds',
        limit_choices_to={
            'is_template': False,
            'assembly': True,
            'active': True,
            'virtual': False,
        },
        help_text=_('Select part to build'),
    )

    sales_order = models.ForeignKey(
        'order.SalesOrder',
        verbose_name=_('Sales Order Reference'),
        on_delete=models.SET_NULL,
        related_name='builds',
        null=True, blank=True,
        help_text=_('SalesOrder to which this build is allocated')
    )
    
    take_from = models.ForeignKey(
        'stock.StockLocation',
        verbose_name=_('Source Location'),
        on_delete=models.SET_NULL,
        related_name='sourcing_builds',
        null=True, blank=True,
        help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)')
    )
    
    quantity = models.PositiveIntegerField(
        verbose_name=_('Build Quantity'),
        default=1,
        validators=[MinValueValidator(1)],
        help_text=_('Number of parts to build')
    )

    status = models.PositiveIntegerField(
        verbose_name=_('Build Status'),
        default=BuildStatus.PENDING,
        choices=BuildStatus.items(),
        validators=[MinValueValidator(0)],
        help_text=_('Build status code')
    )
    
    batch = models.CharField(
        verbose_name=_('Batch Code'),
        max_length=100,
        blank=True,
        null=True,
        help_text=_('Batch code for this build output')
    )
    
    creation_date = models.DateField(auto_now_add=True, editable=False)
    
    completion_date = models.DateField(null=True, blank=True)

    completed_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        blank=True, null=True,
        related_name='builds_completed'
    )
    
    link = InvenTreeURLField(
        verbose_name=_('External Link'),
        blank=True, help_text=_('Link to external URL')
    )

    notes = MarkdownxField(
        verbose_name=_('Notes'),
        blank=True, help_text=_('Extra build notes')
    )

    @property
    def output_count(self):
        return self.build_outputs.count()

    @transaction.atomic
    def cancelBuild(self, user):
        """ Mark the Build as CANCELLED

        - Delete any pending BuildItem objects (but do not remove items from stock)
        - Set build status to CANCELLED
        - Save the Build object
        """

        for item in self.allocated_stock.all():
            item.delete()

        # Date of 'completion' is the date the build was cancelled
        self.completion_date = datetime.now().date()
        self.completed_by = user

        self.status = BuildStatus.CANCELLED
        self.save()

    def getAutoAllocations(self):
        """ Return a list of parts which will be allocated
        using the 'AutoAllocate' function.

        For each item in the BOM for the attached Part:

        - If there is a single StockItem, use that StockItem
        - Take as many parts as available (up to the quantity required for the BOM)
        - If there are multiple StockItems available, ignore (leave up to the user)

        Returns:
            A list object containing the StockItem objects to be allocated (and the quantities)
        """

        allocations = []

        for item in self.part.bom_items.all().prefetch_related('sub_part'):

            # How many parts required for this build?
            q_required = item.quantity * self.quantity

            # Grab a list of StockItem objects which are "in stock"
            stock = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER)
            
            # Filter by part reference
            stock = stock.filter(part=item.sub_part)

            # Ensure that the available stock items are in the correct location
            if self.take_from is not None:
                # Filter for stock that is located downstream of the designated location
                stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])

            # Only one StockItem to choose from? Default to that one!
            if len(stock) == 1:
                stock_item = stock[0]

                # Check that we have not already allocated this stock-item against this build
                build_items = BuildItem.objects.filter(build=self, stock_item=stock_item)

                if len(build_items) > 0:
                    continue

                # Are there any parts available?
                if stock_item.quantity > 0:

                    # Only take as many as are available
                    if stock_item.quantity < q_required:
                        q_required = stock_item.quantity

                    allocation = {
                        'stock_item': stock_item,
                        'quantity': q_required,
                    }

                    allocations.append(allocation)

        return allocations

    @transaction.atomic
    def unallocateStock(self):
        """ Deletes all stock allocations for this build. """

        BuildItem.objects.filter(build=self.id).delete()

    @transaction.atomic
    def autoAllocate(self):
        """ Run auto-allocation routine to allocate StockItems to this Build.

        Returns a list of dict objects with keys like:

            {
                'stock_item': item,
                'quantity': quantity,
            }

        See: getAutoAllocations()
        """

        allocations = self.getAutoAllocations()

        for item in allocations:
            # Create a new allocation
            build_item = BuildItem(
                build=self,
                stock_item=item['stock_item'],
                quantity=item['quantity'])

            build_item.save()

    @transaction.atomic
    def completeBuild(self, location, serial_numbers, user):
        """ Mark the Build as COMPLETE

        - Takes allocated items from stock
        - Delete pending BuildItem objects
        """

        # Complete the build allocation for each BuildItem
        for build_item in self.allocated_stock.all().prefetch_related('stock_item'):
            build_item.complete_allocation(user)

            # Check that the stock-item has been assigned to this build, and remove the builditem from the database
            if build_item.stock_item.build_order == self:
                build_item.delete()

        notes = 'Built {q} on {now}'.format(
            q=self.quantity,
            now=str(datetime.now().date())
        )

        # Generate the build outputs
        if self.part.trackable and serial_numbers:
            # Add new serial numbers
            for serial in serial_numbers:
                item = StockModels.StockItem.objects.create(
                    part=self.part,
                    build=self,
                    location=location,
                    quantity=1,
                    serial=serial,
                    batch=str(self.batch) if self.batch else '',
                    notes=notes
                )

                item.save()

        else:
            # Add stock of the newly created item
            item = StockModels.StockItem.objects.create(
                part=self.part,
                build=self,
                location=location,
                quantity=self.quantity,
                batch=str(self.batch) if self.batch else '',
                notes=notes
            )

            item.save()

        # Finally, mark the build as complete
        self.completion_date = datetime.now().date()
        self.completed_by = user
        self.status = BuildStatus.COMPLETE
        self.save()

        return True

    def isFullyAllocated(self):
        """
        Return True if this build has been fully allocated.
        """

        bom_items = self.part.bom_items.all()

        for item in bom_items:
            part = item.sub_part

            if not self.isPartFullyAllocated(part):
                return False

        return True

    def isPartFullyAllocated(self, part):
        """
        Check if a given Part is fully allocated for this Build
        """

        return self.getAllocatedQuantity(part) >= self.getRequiredQuantity(part)

    def getRequiredQuantity(self, part):
        """ Calculate the quantity of <part> required to make this build.
        """

        try:
            item = PartModels.BomItem.objects.get(part=self.part.id, sub_part=part.id)
            q = item.quantity
        except PartModels.BomItem.DoesNotExist:
            q = 0

        return q * self.quantity

    def getAllocatedQuantity(self, part):
        """ Calculate the total number of <part> currently allocated to this build
        """

        allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(q=Coalesce(Sum('quantity'), 0))

        return allocated['q']

    def getUnallocatedQuantity(self, part):
        """ Calculate the quantity of <part> which still needs to be allocated to this build.

        Args:
            Part - the part to be tested

        Returns:
            The remaining allocated quantity
        """

        return max(self.getRequiredQuantity(part) - self.getAllocatedQuantity(part), 0)

    @property
    def required_parts(self):
        """ Returns a dict of parts required to build this part (BOM) """
        parts = []

        for item in self.part.bom_items.all().prefetch_related('sub_part'):
            part = {
                'part': item.sub_part,
                'per_build': item.quantity,
                'quantity': item.quantity * self.quantity,
                'allocated': self.getAllocatedQuantity(item.sub_part)
            }

            parts.append(part)

        return parts

    @property
    def can_build(self):
        """ Return true if there are enough parts to supply build """

        for item in self.required_parts:
            if item['part'].total_stock < item['quantity']:
                return False

        return True

    @property
    def is_active(self):
        """ Is this build active? An active build is either:

        - PENDING
        - HOLDING
        """

        return self.status in BuildStatus.ACTIVE_CODES

    @property
    def is_complete(self):
        """ Returns True if the build status is COMPLETE """
        return self.status == BuildStatus.COMPLETE
예제 #27
0
class Company(models.Model):
    """ A Company object represents an external company.
    It may be a supplier or a customer (or both).

    Attributes:
        name: Brief name of the company
        description: Longer form description
        website: URL for the company website
        address: Postal address
        phone: contact phone number
        email: contact email address
        URL: Secondary URL e.g. for link to internal Wiki page
        image: Company image / logo
        notes: Extra notes about the company
        is_customer: boolean value, is this company a customer
        is_supplier: boolean value, is this company a supplier
    """

    name = models.CharField(max_length=100,
                            blank=False,
                            unique=True,
                            help_text=_('Company name'))

    description = models.CharField(max_length=500,
                                   help_text=_('Description of the company'))

    website = models.URLField(blank=True, help_text=_('Company website URL'))

    address = models.CharField(max_length=200,
                               blank=True,
                               help_text=_('Company address'))

    phone = models.CharField(max_length=50,
                             blank=True,
                             help_text=_('Contact phone number'))

    email = models.EmailField(blank=True, help_text=_('Contact email address'))

    contact = models.CharField(max_length=100,
                               blank=True,
                               help_text=_('Point of contact'))

    URL = InvenTreeURLField(
        blank=True, help_text=_('Link to external company information'))

    image = models.ImageField(upload_to=rename_company_image,
                              max_length=255,
                              null=True,
                              blank=True)

    notes = MarkdownxField(blank=True)

    is_customer = models.BooleanField(
        default=False, help_text=_('Do you sell items to this company?'))

    is_supplier = models.BooleanField(
        default=True, help_text=_('Do you purchase items from this company?'))

    def __str__(self):
        """ Get string representation of a Company """
        return "{n} - {d}".format(n=self.name, d=self.description)

    def get_absolute_url(self):
        """ Get the web URL for the detail view for this Company """
        return reverse('company-detail', kwargs={'pk': self.id})

    def get_image_url(self):
        """ Return the URL of the image for this company """

        if self.image:
            return os.path.join(settings.MEDIA_URL, str(self.image.url))
        else:
            return os.path.join(settings.STATIC_URL, 'img/blank_image.png')

    @property
    def part_count(self):
        """ The number of parts supplied by this company """
        return self.parts.count()

    @property
    def has_parts(self):
        """ Return True if this company supplies any parts """
        return self.part_count > 0

    @property
    def stock_items(self):
        """ Return a list of all stock items supplied by this company """
        stock = apps.get_model('stock', 'StockItem')
        return stock.objects.filter(supplier_part__supplier=self.id).all()

    @property
    def stock_count(self):
        """ Return the number of stock items supplied by this company """
        stock = apps.get_model('stock', 'StockItem')
        return stock.objects.filter(supplier_part__supplier=self.id).count()

    def outstanding_purchase_orders(self):
        """ Return purchase orders which are 'outstanding' """
        return self.purchase_orders.filter(status__in=OrderStatus.OPEN)

    def pending_purchase_orders(self):
        """ Return purchase orders which are PENDING (not yet issued) """
        return self.purchase_orders.filter(status=OrderStatus.PENDING)

    def closed_purchase_orders(self):
        """ Return purchase orders which are not 'outstanding'

        - Complete
        - Failed / lost
        - Returned
        """

        return self.purchase_orders.exclude(status__in=OrderStatus.OPEN)

    def complete_purchase_orders(self):
        return self.purchase_orders.filter(status=OrderStatus.COMPLETE)

    def failed_purchase_orders(self):
        """ Return any purchase orders which were not successful """

        return self.purchase_orders.filter(status__in=OrderStatus.FAILED)
예제 #28
0
class MissionStatement(AbstractConference, SingleActiveModel):
    content = MarkdownxField(null=True, blank=True)

    def __str__(self):
        return self.content
예제 #29
0
파일: models.py 프로젝트: mammique/diamind
class Entry(models.Model):

    name = models.CharField(max_length=64, blank=True)
    aka = models.CharField('a.k.a', max_length=256, blank=True)
    name_prefix = models.ForeignKey(
        'self',
        verbose_name="Entry name prefix",
        help_text=
        "Use this parent/tag entry's name as prefix when displayed out of context.",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="children_using_name")
    text = MarkdownxField(blank=True)
    file = models.FileField(upload_to='file', blank=True)
    image = models.ImageField(upload_to='image', blank=True)
    home = models.BooleanField(default=False,
                               help_text="Display entry in home page.")
    date = models.DateTimeField(auto_now_add=True)
    updated_on = models.DateTimeField(auto_now=True)
    parents = models.ManyToManyField(
        'self',
        related_name="children",
        symmetrical=False,
        blank=True,
        through='EntryParentThroughModel',
        through_fields=('child', 'parent'),
    )
    tags = models.ManyToManyField(
        'self',
        related_name="tagged_from",
        symmetrical=False,
        blank=True,
    )

    order_with_respect_to = 'pk'

    def file_ext(self):
        if self.file: return self.file.name.split('.')[-1].lower()

    def file_is_video(self):
        return self.file_ext() in (
            'mp4',
            'ogg',
            'webm',
        )

    def file_filename(self):
        return os.path.basename(self.file.name)

    def text_html(self):
        return mark_safe(markdownify(self.text))

    def badge(self):
        t = Template(
            """<a class="badge badge-secondary entry" href="%s">%s</a>""" %
            (self.url_path(), self.span()))
        return t.render(TemplateContext({'entry': self}))

    def span(self, name_prefix=True):

        if self.image:
            img = """{% load responsive_images %}<img src="{% src entry.image 32x24 nocrop %}" />&nbsp;"""
        else:
            img = ''

        t = Template("""<span>%s{{ name }}</span>""" % img)

        return t.render(
            TemplateContext({
                'entry': self,
                'name': self.name_get(name_prefix=name_prefix)
            }))

    def span_no_name_prefix(self):
        return self.span(name_prefix=False)

    def url_path(self):
        return reverse('nav', args=('%s/' % self.pk, ))

    def name_get(self, name_prefix=True):

        name_strip = self.name.strip()
        text_strip = self.text.strip()

        if name_strip != '': name = name_strip
        elif text_strip != '':
            name = text_strip[0:16]
            if len(name) != len(text_strip): name = '%s…' % name
        else: name = '∅'

        if name_prefix and self.name_prefix:
            name = '%s: %s' % (
                self.name_prefix.name_get_no_name_prefix(),
                name,
            )

        return name

    def name_get_no_name_prefix(self):
        return self.name_get(name_prefix=False)

    def __str__(self):
        return self.name_get()

    class Meta:
        verbose_name_plural = "Entries"