diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index d0695c4b7d..7247caa8d9 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -69,7 +69,7 @@ logger = logging.getLogger(__name__) # Read the autogenerated key-file key_file_name = os.path.join(BASE_DIR, 'secret_key.txt') -logger.info(f'Loading SERCRET_KEY from {key_file_name}') +logger.info(f'Loading SECRET_KEY from {key_file_name}') key_file = open(key_file_name, 'r') SECRET_KEY = key_file.read().strip() diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 2401e9d936..ffc8d97a92 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -46,6 +46,8 @@ class BuildList(generics.ListCreateAPIView): queryset = super().get_queryset().prefetch_related('part') + queryset = BuildSerializer.annotate_queryset(queryset) + return queryset def filter_queryset(self, queryset): @@ -71,6 +73,17 @@ class BuildList(generics.ListCreateAPIView): else: queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES) + # Filter by "overdue" status? + overdue = params.get('overdue', None) + + if overdue is not None: + overdue = str2bool(overdue) + + if overdue: + queryset = queryset.filter(Build.OVERDUE_FILTER) + else: + queryset = queryset.exclude(Build.OVERDUE_FILTER) + # Filter by associated part? part = params.get('part', None) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index ba7787798d..70deeef7b1 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -32,6 +32,14 @@ class EditBuildForm(HelperForm): 'reference': _('Build Order reference') } + # TODO: Make this a more "presentable" date picker + # TODO: Currently does not render super nicely with crispy forms + target_date = forms.DateField( + widget=forms.DateInput( + attrs={'type': 'date'} + ) + ) + class Meta: model = Build fields = [ @@ -40,6 +48,7 @@ class EditBuildForm(HelperForm): 'part', 'quantity', 'batch', + 'target_date', 'take_from', 'destination', 'parent', diff --git a/InvenTree/build/migrations/0025_build_target_date.py b/InvenTree/build/migrations/0025_build_target_date.py new file mode 100644 index 0000000000..e834d74d0f --- /dev/null +++ b/InvenTree/build/migrations/0025_build_target_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-12-15 12:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0024_auto_20201201_1023'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='target_date', + field=models.DateField(blank=True, help_text='Target date for build completion. Build will be overdue after this date.', null=True, verbose_name='Target completion date'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 8616e11b2a..488a8b79e8 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -14,7 +14,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction -from django.db.models import Sum +from django.db.models import Sum, Q from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator @@ -47,11 +47,14 @@ class Build(MPTTModel): 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 + 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") @@ -164,6 +167,12 @@ class Build(MPTTModel): 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( @@ -183,6 +192,22 @@ class Build(MPTTModel): 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): """ diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 4423619b8a..b71aaecc61 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -5,12 +5,17 @@ JSON serializers for Build API # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db.models import Case, When, Value +from django.db.models import BooleanField + from rest_framework import serializers + from InvenTree.serializers import InvenTreeModelSerializer + from stock.serializers import StockItemSerializerBrief +from part.serializers import PartBriefSerializer from .models import Build, BuildItem -from part.serializers import PartBriefSerializer class BuildSerializer(InvenTreeModelSerializer): @@ -23,6 +28,33 @@ class BuildSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() + overdue = serializers.BooleanField() + + @staticmethod + def annotate_queryset(queryset): + """ + Add custom annotations to the BuildSerializer queryset, + performing database queries as efficiently as possible. + + The following annoted fields are added: + + - overdue: True if the build is outstanding *and* the completion date has past + + """ + + # Annotate a boolean 'overdue' flag + + queryset = queryset.annotate( + overdue=Case( + When( + Build.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()) + ) + ) + + return queryset + def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) @@ -42,11 +74,13 @@ class BuildSerializer(InvenTreeModelSerializer): 'completion_date', 'part', 'part_detail', + 'overdue', 'reference', 'sales_order', 'quantity', 'status', 'status_text', + 'target_date', 'notes', 'link', ] diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 3a94398e87..1124dd16c0 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -37,7 +37,12 @@ src="{% static 'img/blank_image.png' %}" {% endif %} -
{{ build.title }}
- | {% trans "BOM Price" %} | -
- {% if bom_price %}
- {{ bom_price }}
- {% if build.part.has_complete_bom_pricing == False %}
- {% trans "BOM pricing is incomplete" %} - {% endif %} - {% else %} - {% trans "No pricing information" %} - {% endif %} - |
- |
- | {% trans "Completed" %} | -{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %} | -|
+ | {% trans "Target Date" %} | + {% if build.target_date %} ++ {{ build.target_date }}{% if build.is_overdue %} {% endif %} + | + {% else %} +{% trans "No target date set" %} | {% endif %} - +
+ | {% trans "Completed" %} | + {% if build.completion_date %} +{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %} | + {% else %} +{% trans "Build not complete" %} | + {% endif %} +