Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-12-16 21:30:08 +11:00
commit ffbeb0186e
19 changed files with 1637 additions and 1248 deletions

View File

@ -69,7 +69,7 @@ logger = logging.getLogger(__name__)
# Read the autogenerated key-file # Read the autogenerated key-file
key_file_name = os.path.join(BASE_DIR, 'secret_key.txt') 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') key_file = open(key_file_name, 'r')
SECRET_KEY = key_file.read().strip() SECRET_KEY = key_file.read().strip()

View File

@ -46,6 +46,8 @@ class BuildList(generics.ListCreateAPIView):
queryset = super().get_queryset().prefetch_related('part') queryset = super().get_queryset().prefetch_related('part')
queryset = BuildSerializer.annotate_queryset(queryset)
return queryset return queryset
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
@ -71,6 +73,17 @@ class BuildList(generics.ListCreateAPIView):
else: else:
queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES) 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? # Filter by associated part?
part = params.get('part', None) part = params.get('part', None)

View File

@ -32,6 +32,14 @@ class EditBuildForm(HelperForm):
'reference': _('Build Order reference') '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: class Meta:
model = Build model = Build
fields = [ fields = [
@ -40,6 +48,7 @@ class EditBuildForm(HelperForm):
'part', 'part',
'quantity', 'quantity',
'batch', 'batch',
'target_date',
'take_from', 'take_from',
'destination', 'destination',
'parent', 'parent',

View File

@ -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'),
),
]

View File

@ -14,7 +14,7 @@ from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from django.db import models, transaction 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.db.models.functions import Coalesce
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -47,11 +47,14 @@ class Build(MPTTModel):
status: Build status code status: Build status code
batch: Batch code transferred to build parts (optional) batch: Batch code transferred to build parts (optional)
creation_date: Date the build was created (auto) 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 link: External URL for extra information
notes: Text notes notes: Text notes
""" """
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
class Meta: class Meta:
verbose_name = _("Build Order") verbose_name = _("Build Order")
verbose_name_plural = _("Build Orders") verbose_name_plural = _("Build Orders")
@ -164,6 +167,12 @@ class Build(MPTTModel):
creation_date = models.DateField(auto_now_add=True, editable=False) 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) completion_date = models.DateField(null=True, blank=True)
completed_by = models.ForeignKey( completed_by = models.ForeignKey(
@ -183,6 +192,22 @@ class Build(MPTTModel):
blank=True, help_text=_('Extra build 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 @property
def active(self): def active(self):
""" """

View File

@ -5,12 +5,17 @@ JSON serializers for Build API
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals 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 rest_framework import serializers
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from stock.serializers import StockItemSerializerBrief from stock.serializers import StockItemSerializerBrief
from part.serializers import PartBriefSerializer
from .models import Build, BuildItem from .models import Build, BuildItem
from part.serializers import PartBriefSerializer
class BuildSerializer(InvenTreeModelSerializer): class BuildSerializer(InvenTreeModelSerializer):
@ -23,6 +28,33 @@ class BuildSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField() 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): def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False) part_detail = kwargs.pop('part_detail', False)
@ -42,11 +74,13 @@ class BuildSerializer(InvenTreeModelSerializer):
'completion_date', 'completion_date',
'part', 'part',
'part_detail', 'part_detail',
'overdue',
'reference', 'reference',
'sales_order', 'sales_order',
'quantity', 'quantity',
'status', 'status',
'status_text', 'status_text',
'target_date',
'notes', 'notes',
'link', 'link',
] ]

View File

@ -37,7 +37,12 @@ src="{% static 'img/blank_image.png' %}"
<a href="{% url 'admin:build_build_change' build.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a> <a href="{% url 'admin:build_build_change' build.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
{% endif %} {% endif %}
</h3> </h3>
<h3>{% build_status_label build.status large=True %}</h3> <h3>
{% build_status_label build.status large=True %}
{% if build.is_overdue %}
<span class='label label-large label-large-red'>{% trans "Overdue" %}</span>
{% endif %}
</h3>
<hr> <hr>
<p>{{ build.title }}</p> <p>{{ build.title }}</p>
<div class='btn-row'> <div class='btn-row'>
@ -81,7 +86,12 @@ src="{% static 'img/blank_image.png' %}"
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td> <td>{% trans "Status" %}</td>
<td>{% build_status_label build.status %}</td> <td>
{% build_status_label build.status %}
{% if build.is_overdue %}
<span title='{% trans "This build was due on" %} {{ build.target_date }}' class='label label-red'>{% trans "Overdue" %}</span>
{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<td><span class='fas fa-spinner'></span></td> <td><span class='fas fa-spinner'></span></td>

View File

@ -95,33 +95,26 @@
<td>{% trans "Created" %}</td> <td>{% trans "Created" %}</td>
<td>{{ build.creation_date }}</td> <td>{{ build.creation_date }}</td>
</tr> </tr>
</table> <tr>
</div> <td><span class='fas fa-calendar-alt'></span></td>
<div class='col-sm-6'> <td>{% trans "Target Date" %}</td>
<table class='table table-striped'> {% if build.target_date %}
<col width='25'> <td>
<tr> {{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
<td><span class='fas fa-dollar-sign'></span></td> </td>
<td>{% trans "BOM Price" %}</td> {% else %}
<td> <td><i>{% trans "No target date set" %}</i></td>
{% if bom_price %}
{{ bom_price }}
{% if build.part.has_complete_bom_pricing == False %}
<br><span class='warning-msg'><i>{% trans "BOM pricing is incomplete" %}</i></span>
{% endif %}
{% else %}
<span class='warning-msg'><i>{% trans "No pricing information" %}</i></span>
{% endif %}
</td>
</tr>
{% if build.completion_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Completed" %}</td>
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
</tr>
{% endif %} {% endif %}
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Completed" %}</td>
{% if build.completion_date %}
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
{% else %}
<td><i>{% trans "Build not complete" %}</i></td>
{% endif %}
</tr>
</table> </table>
</div> </div>
</div> </div>

View File

@ -721,7 +721,7 @@ class BuildUpdate(AjaxUpdateView):
model = Build model = Build
form_class = forms.EditBuildForm form_class = forms.EditBuildForm
context_object_name = 'build' context_object_name = 'build'
ajax_form_title = _('Edit Build Details') ajax_form_title = _('Edit Build Order Details')
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
role_required = 'build.change' role_required = 'build.change'
@ -764,7 +764,7 @@ class BuildDelete(AjaxDeleteView):
model = Build model = Build
ajax_template_name = 'build/delete_build.html' ajax_template_name = 'build/delete_build.html'
ajax_form_title = _('Delete Build') ajax_form_title = _('Delete Build Order')
role_required = 'build.delete' role_required = 'build.delete'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
{% extends "collapse_index.html" %}
{% load i18n %}
{% block collapse_title %}
<span class='fas fa-calendar-times icon-header'></span>
{% trans "Overdue Builds" %}<span class='badge' id='build-overdue-count'><span class='fas fa-spin fa-spinner'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='build-overdue-table'>
</table>
{% endblock %}

View File

@ -16,6 +16,7 @@ InvenTree | {% trans "Index" %}
{% endif %} {% endif %}
{% if roles.build.view %} {% if roles.build.view %}
{% include "InvenTree/build_pending.html" with collapse_id="build_pending" %} {% include "InvenTree/build_pending.html" with collapse_id="build_pending" %}
{% include "InvenTree/build_overdue.html" with collapse_id="build_overdue" %}
{% endif %} {% endif %}
</div> </div>
<div class='col-sm-6'> <div class='col-sm-6'>
@ -72,6 +73,15 @@ loadBuildTable("#build-pending-table", {
disableFilters: true, disableFilters: true,
}); });
loadBuildTable("#build-overdue-table", {
url: "{% url 'api-build-list' %}",
params: {
part_detail: true,
overdue: true,
},
disableFilters: true,
});
loadSimplePartTable("#low-stock-table", "{% url 'api-part-list' %}", { loadSimplePartTable("#low-stock-table", "{% url 'api-part-list' %}", {
params: { params: {
low_stock: true, low_stock: true,
@ -126,6 +136,12 @@ $("#build-pending-table").on('load-success.bs.table', function() {
$("#build-pending-count").html(count); $("#build-pending-count").html(count);
}); });
$("#build-overdue-table").on('load-success.bs.table', function() {
var count = $("#build-overdue-table").bootstrapTable('getData').length;
$("#build-overdue-count").html(count);
});
$("#low-stock-table").on('load-success.bs.table', function() { $("#low-stock-table").on('load-success.bs.table', function() {
var count = $("#low-stock-table").bootstrapTable('getData').length; var count = $("#low-stock-table").bootstrapTable('getData').length;

View File

@ -650,7 +650,13 @@ function loadBuildTable(table, options) {
value = `${prefix}${value}`; value = `${prefix}${value}`;
} }
return renderLink(value, '/build/' + row.pk + '/'); var html = renderLink(value, '/build/' + row.pk + '/');
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Build order is overdue" %}');
}
return html;
} }
}, },
{ {
@ -699,6 +705,11 @@ function loadBuildTable(table, options) {
title: '{% trans "Created" %}', title: '{% trans "Created" %}',
sortable: true, sortable: true,
}, },
{
field: 'target_date',
title: '{% trans "Target Date" %}',
sortable: true,
},
{ {
field: 'completion_date', field: 'completion_date',
title: '{% trans "Completed" %}', title: '{% trans "Completed" %}',

View File

@ -202,7 +202,7 @@ function loadPartVariantTable(table, partId, options={}) {
showColumns: true, showColumns: true,
original: params, original: params,
queryParams: filters, queryParams: filters,
formatNoMatches: function() { return "{% trans "No variants found" %}"; }, formatNoMatches: function() { return '{% trans "No variants found" %}'; },
columns: cols, columns: cols,
treeEnable: true, treeEnable: true,
rootParentId: partId, rootParentId: partId,
@ -249,7 +249,7 @@ function loadParametricPartTable(table, options={}) {
if (header === 'part') { if (header === 'part') {
columns.push({ columns.push({
field: header, field: header,
title: '{% trans 'Part' %}', title: '{% trans "Part" %}',
sortable: true, sortable: true,
sortName: 'name', sortName: 'name',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
@ -268,7 +268,7 @@ function loadParametricPartTable(table, options={}) {
} else if (header === 'description') { } else if (header === 'description') {
columns.push({ columns.push({
field: header, field: header,
title: '{% trans 'Description' %}', title: '{% trans "Description" %}',
sortable: true, sortable: true,
}); });
} else { } else {
@ -288,7 +288,7 @@ function loadParametricPartTable(table, options={}) {
queryParams: table_headers, queryParams: table_headers,
groupBy: false, groupBy: false,
name: options.name || 'parametric', name: options.name || 'parametric',
formatNoMatches: function() { return "{% trans "No parts found" %}"; }, formatNoMatches: function() { return '{% trans "No parts found" %}'; },
columns: columns, columns: columns,
showColumns: true, showColumns: true,
data: table_data, data: table_data,
@ -454,7 +454,7 @@ function loadPartTable(table, url, options={}) {
groupBy: false, groupBy: false,
name: options.name || 'part', name: options.name || 'part',
original: params, original: params,
formatNoMatches: function() { return "{% trans "No parts found" %}"; }, formatNoMatches: function() { return '{% trans "No parts found" %}'; },
columns: columns, columns: columns,
showColumns: true, showColumns: true,
}); });
@ -564,12 +564,12 @@ function loadPartTestTemplateTable(table, options) {
}, },
{ {
field: 'test_name', field: 'test_name',
title: "{% trans "Test Name" %}", title: '{% trans "Test Name" %}',
sortable: true, sortable: true,
}, },
{ {
field: 'description', field: 'description',
title: "{% trans "Description" %}", title: '{% trans "Description" %}',
}, },
{ {
field: 'required', field: 'required',
@ -581,14 +581,14 @@ function loadPartTestTemplateTable(table, options) {
}, },
{ {
field: 'requires_value', field: 'requires_value',
title: "{% trans "Requires Value" %}", title: '{% trans "Requires Value" %}',
formatter: function(value) { formatter: function(value) {
return yesNoLabel(value); return yesNoLabel(value);
} }
}, },
{ {
field: 'requires_attachment', field: 'requires_attachment',
title: "{% trans "Requires Attachment" %}", title: '{% trans "Requires Attachment" %}',
formatter: function(value) { formatter: function(value) {
return yesNoLabel(value); return yesNoLabel(value);
} }
@ -608,7 +608,9 @@ function loadPartTestTemplateTable(table, options) {
return html; return html;
} else { } else {
return '{% trans "This test is defined for a parent part" %}'; var text = '{% trans "This test is defined for a parent part" %}';
return renderLink(text, `/part/${row.part}/tests/`);
} }
} }
} }

View File

@ -184,7 +184,11 @@ function getAvailableTableFilters(tableKey) {
active: { active: {
type: 'bool', type: 'bool',
title: '{% trans "Active" %}', title: '{% trans "Active" %}',
} },
overdue: {
type: 'bool',
title: '{% trans "Overdue" %}',
},
}; };
} }

View File

@ -1,4 +1,5 @@
{% load static %} {% load static %}
{% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -26,22 +27,25 @@
<div class='login'> <div class='login'>
<div class="row"> <div class="row">
<div class="col-md-2 col-md-offset-5"> <div class='container-fluid'>
<div class='clearfix content-heading'> <div class='clearfix content-heading'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/> <h3>InvenTree</h3> <img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/> <h3>InvenTree</h3>
</div> </div>
<hr> <hr>
<div class='container-fluid'> <div class='container-fluid'>
<form method="post"> <form method="post" action=''>
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {% load crispy_forms_tags %}
<button class='pull-right btn btn-primary' type="submit">Login</button>
</form> {{ form|crispy }}
<button class='pull-right btn btn-primary' type="submit">{% trans "Login" %}</button>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>