Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-12-18 20:08:41 +11:00
commit 7050b3a410
16 changed files with 169 additions and 34 deletions

View File

@ -153,11 +153,13 @@ class InvenTreeSetting(models.Model):
'SALESORDER_REFERENCE_PREFIX': { 'SALESORDER_REFERENCE_PREFIX': {
'name': _('Sales Order Reference Prefix'), 'name': _('Sales Order Reference Prefix'),
'description': _('Prefix value for sales order reference'), 'description': _('Prefix value for sales order reference'),
'default': 'SO',
}, },
'PURCHASEORDER_REFERENCE_PREFIX': { 'PURCHASEORDER_REFERENCE_PREFIX': {
'name': _('Purchase Order Reference Prefix'), 'name': _('Purchase Order Reference Prefix'),
'description': _('Prefix value for purchase order reference'), 'description': _('Prefix value for purchase order reference'),
'default': 'PO',
}, },
} }

View File

@ -266,6 +266,17 @@ class SOList(generics.ListCreateAPIView):
else: else:
queryset = queryset.exclude(status__in=SalesOrderStatus.OPEN) queryset = queryset.exclude(status__in=SalesOrderStatus.OPEN)
# Filter by 'overdue' status
overdue = params.get('overdue', None)
if overdue is not None:
overdue = str2bool(overdue)
if overdue:
queryset = queryset.filter(SalesOrder.OVERDUE_FILTER)
else:
queryset = queryset.exclude(SalesOrder.OVERDUE_FILTER)
status = params.get('status', None) status = params.get('status', None)
if status is not None: if status is not None:

View File

@ -128,6 +128,13 @@ class EditSalesOrderForm(HelperForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# TODO: Improve this using a better date picker
target_date = forms.DateField(
widget=forms.DateInput(
attrs={'type': 'date'},
)
)
class Meta: class Meta:
model = SalesOrder model = SalesOrder
fields = [ fields = [
@ -135,6 +142,7 @@ class EditSalesOrderForm(HelperForm):
'customer', 'customer',
'customer_reference', 'customer_reference',
'description', 'description',
'target_date',
'link' 'link'
] ]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-12-18 01:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0039_auto_20201112_2203'),
]
operations = [
migrations.AddField(
model_name='salesorder',
name='target_date',
field=models.DateField(blank=True, help_text='Target date for order completion. Order will be overdue after this date.', null=True, verbose_name='Target completion date'),
),
]

View File

@ -9,7 +9,7 @@ from datetime import datetime
from decimal import Decimal from decimal import Decimal
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F, Sum from django.db.models import Q, F, Sum
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
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -26,7 +26,7 @@ from stock import models as stock_models
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from InvenTree.fields import RoundingDecimalField from InvenTree.fields import RoundingDecimalField
from InvenTree.helpers import decimal2string, increment from InvenTree.helpers import decimal2string, increment, getSetting
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
from InvenTree.models import InvenTreeAttachment from InvenTree.models import InvenTreeAttachment
@ -49,8 +49,6 @@ class Order(models.Model):
""" """
ORDER_PREFIX = ""
@classmethod @classmethod
def getNextOrderNumber(cls): def getNextOrderNumber(cls):
""" """
@ -88,16 +86,6 @@ class Order(models.Model):
return new_ref 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): def save(self, *args, **kwargs):
if not self.creation_date: if not self.creation_date:
self.creation_date = datetime.now().date() self.creation_date = datetime.now().date()
@ -133,10 +121,11 @@ class PurchaseOrder(Order):
received_by: User that received the goods received_by: User that received the goods
""" """
ORDER_PREFIX = "PO"
def __str__(self): def __str__(self):
return "PO {ref} - {company}".format(ref=self.reference, company=self.supplier.name)
prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX')
return f"{prefix}{self.reference} - {self.supplier.name}"
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(), status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
help_text=_('Purchase order status')) help_text=_('Purchase order status'))
@ -307,10 +296,16 @@ class SalesOrder(Order):
Attributes: Attributes:
customer: Reference to the company receiving the goods in the order customer: Reference to the company receiving the goods in the order
customer_reference: Optional field for customer order reference code customer_reference: Optional field for customer order reference code
target_date: Target date for SalesOrder completion (optional)
""" """
OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
def __str__(self): def __str__(self):
return "SO {ref} - {company}".format(ref=self.reference, company=self.customer.name)
prefix = getSetting('SALESORDER_REFERENCE_PREFIX')
return f"{prefix}{self.reference} - {self.customer.name}"
def get_absolute_url(self): def get_absolute_url(self):
return reverse('so-detail', kwargs={'pk': self.id}) return reverse('so-detail', kwargs={'pk': self.id})
@ -329,6 +324,12 @@ class SalesOrder(Order):
customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code")) customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code"))
target_date = models.DateField(
null=True, blank=True,
verbose_name=_('Target completion date'),
help_text=_('Target date for order completion. Order will be overdue after this date.')
)
shipment_date = models.DateField(blank=True, null=True) shipment_date = models.DateField(blank=True, null=True)
shipped_by = models.ForeignKey( shipped_by = models.ForeignKey(
@ -338,6 +339,23 @@ class SalesOrder(Order):
related_name='+' related_name='+'
) )
@property
def is_overdue(self):
"""
Returns true if this SalesOrder is "overdue":
- Not completed
- Target date is "in the past"
"""
# Order cannot be deemed overdue if target_date is not set
if self.target_date is None:
return False
today = datetime.now().date()
return self.is_pending and self.target_date < today
@property @property
def is_pending(self): def is_pending(self):
return self.status == SalesOrderStatus.PENDING return self.status == SalesOrderStatus.PENDING

View File

@ -9,6 +9,9 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount from sql_util.utils import SubqueryCount
from django.db.models import Case, When, Value
from django.db.models import BooleanField
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField from InvenTree.serializers import InvenTreeAttachmentSerializerField
@ -152,12 +155,24 @@ class SalesOrderSerializer(InvenTreeModelSerializer):
def annotate_queryset(queryset): def annotate_queryset(queryset):
""" """
Add extra information to the queryset Add extra information to the queryset
- Number of line items in the SalesOrder
- Overdue status of the SalesOrder
""" """
queryset = queryset.annotate( queryset = queryset.annotate(
line_items=SubqueryCount('lines') line_items=SubqueryCount('lines')
) )
queryset = queryset.annotate(
overdue=Case(
When(
SalesOrder.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField())
)
)
return queryset return queryset
customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True) customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True)
@ -166,24 +181,27 @@ class SalesOrderSerializer(InvenTreeModelSerializer):
status_text = serializers.CharField(source='get_status_display', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True)
overdue = serializers.BooleanField()
class Meta: class Meta:
model = SalesOrder model = SalesOrder
fields = [ fields = [
'pk', 'pk',
'shipment_date',
'creation_date', 'creation_date',
'description',
'line_items',
'link',
'reference',
'customer', 'customer',
'customer_detail', 'customer_detail',
'customer_reference', 'customer_reference',
'description',
'line_items',
'link',
'notes',
'overdue',
'reference',
'status', 'status',
'status_text', 'status_text',
'shipment_date', 'shipment_date',
'notes', 'target_date',
] ]
read_only_fields = [ read_only_fields = [

View File

@ -37,6 +37,9 @@ src="{% static 'img/blank_image.png' %}"
</h3> </h3>
<h3> <h3>
{% sales_order_status_label order.status large=True %} {% sales_order_status_label order.status large=True %}
{% if order.is_overdue %}
<span class='label label-large label-large-red'>{% trans "Overdue" %}</span>
{% endif %}
</h3> </h3>
<hr> <hr>
<p>{{ order.description }}</p> <p>{{ order.description }}</p>
@ -74,7 +77,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 "Order Status" %}</td> <td>{% trans "Order Status" %}</td>
<td>{% sales_order_status_label order.status %}</td> <td>
{% sales_order_status_label order.status %}
{% if order.is_overdue %}
<span class='label label-red'>{% trans "Overdue" %}</span>
{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<td><span class='fas fa-building'></span></td> <td><span class='fas fa-building'></span></td>
@ -100,6 +108,13 @@ src="{% static 'img/blank_image.png' %}"
<td>{% trans "Created" %}</td> <td>{% trans "Created" %}</td>
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td> <td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
</tr> </tr>
{% if order.target_date %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Target Date" %}</td>
<td>{{ order.target_date }}</td>
</tr>
{% endif %}
{% if order.shipment_date %} {% if order.shipment_date %}
<tr> <tr>
<td><span class='fas fa-truck'></span></td> <td><span class='fas fa-truck'></span></td>

View File

@ -130,7 +130,7 @@ class CategoryParameters(generics.ListAPIView):
""" """
try: try:
cat_id = int(self.request.query_params.get('category', None)) cat_id = int(self.kwargs.get('pk', None))
except TypeError: except TypeError:
cat_id = None cat_id = None
fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true')) fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true'))
@ -910,8 +910,8 @@ part_api_urls = [
# Base URL for PartCategory API endpoints # Base URL for PartCategory API endpoints
url(r'^category/', include([ url(r'^category/', include([
url(r'^(?P<pk>\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'), url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
url(r'^parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
url(r'^$', CategoryList.as_view(), name='api-part-category-list'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
])), ])),

View File

@ -594,7 +594,7 @@ class Part(MPTTModel):
# User can decide whether duplicate IPN (Internal Part Number) values are allowed # User can decide whether duplicate IPN (Internal Part Number) values are allowed
allow_duplicate_ipn = common.models.InvenTreeSetting.get_setting('PART_ALLOW_DUPLICATE_IPN') allow_duplicate_ipn = common.models.InvenTreeSetting.get_setting('PART_ALLOW_DUPLICATE_IPN')
if not allow_duplicate_ipn: if self.IPN is not None and not allow_duplicate_ipn:
parts = Part.objects.filter(IPN__iexact=self.IPN) parts = Part.objects.filter(IPN__iexact=self.IPN)
parts = parts.exclude(pk=self.pk) parts = parts.exclude(pk=self.pk)

View File

@ -114,7 +114,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<!-- Stock adjustment menu --> <!-- Stock adjustment menu -->
{% if roles.stock.change and not item.is_building %} {% if roles.stock.change and not item.is_building %}
<div class='btn-group'> <div class='btn-group'>
<button id='stock-options' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button> <button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'> <ul class='dropdown-menu' role='menu'>
{% if item.in_stock %} {% if item.in_stock %}
{% if not item.serialized %} {% if not item.serialized %}

View File

@ -29,6 +29,7 @@ InvenTree | {% trans "Index" %}
{% endif %} {% endif %}
{% if roles.sales_order.view %} {% if roles.sales_order.view %}
{% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %} {% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %}
{% include "InvenTree/so_overdue.html" with collapse_id="so_overdue" %}
{% endif %} {% endif %}
</div> </div>
@ -112,6 +113,14 @@ loadSalesOrderTable("#so-outstanding-table", {
}, },
}); });
loadSalesOrderTable("#so-overdue-table", {
url: "{% url 'api-so-list' %}",
params: {
overdue: true,
customer_detail: true,
}
});
$("#latest-parts-table").on('load-success.bs.table', function() { $("#latest-parts-table").on('load-success.bs.table', function() {
var count = $("#latest-parts-table").bootstrapTable('getData').length; var count = $("#latest-parts-table").bootstrapTable('getData').length;
@ -166,4 +175,10 @@ $("#so-outstanding-table").on('load-success.bs.table', function() {
$("#so-outstanding-count").html(count); $("#so-outstanding-count").html(count);
}); });
$("#so-overdue-table").on('load-success.bs.table', function() {
var count = $("#so-overdue-table").bootstrapTable('getData').length;
$("#so-overdue-count").html(count);
});
{% endblock %} {% endblock %}

View File

@ -45,7 +45,7 @@
{% if category %} {% if category %}
$("#param-table").inventreeTable({ $("#param-table").inventreeTable({
url: "{% url 'api-part-category-parameters' category.pk %}", url: "{% url 'api-part-category-parameters' pk=category.pk %}",
queryParams: { queryParams: {
ordering: 'name', ordering: 'name',
}, },
@ -58,7 +58,7 @@
switchable: false, switchable: false,
}, },
{ {
field: 'parameter_template_detail.name', field: 'parameter_template.name',
title: '{% trans "Parameter Template" %}', title: '{% trans "Parameter Template" %}',
sortable: 'true', sortable: 'true',
}, },

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 Sales Orders" %}<span class='badge' id='so-overdue-count'><span class='fas fa-spin fa-spinner'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='so-overdue-table'>
</table>
{% endblock %}

View File

@ -235,7 +235,13 @@ function loadSalesOrderTable(table, options) {
value = `${prefix}${value}`; value = `${prefix}${value}`;
} }
return renderLink(value, `/order/sales-order/${row.pk}/`); var html = renderLink(value, `/order/sales-order/${row.pk}/`);
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
}
return html;
}, },
}, },
{ {
@ -269,6 +275,11 @@ function loadSalesOrderTable(table, options) {
field: 'creation_date', field: 'creation_date',
title: '{% trans "Creation Date" %}', title: '{% trans "Creation Date" %}',
}, },
{
sortable: true,
field: 'target_date',
title: '{% trans "Target Date" %}',
},
{ {
sortable: true, sortable: true,
field: 'shipment_date', field: 'shipment_date',

View File

@ -217,6 +217,10 @@ function getAvailableTableFilters(tableKey) {
type: 'bool', type: 'bool',
title: '{% trans "Outstanding" %}', title: '{% trans "Outstanding" %}',
}, },
overdue: {
type: 'bool',
title: '{% trans "Overdue" %}',
},
}; };
} }