diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 2f27dd602d..fa1d4dc5c9 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -153,11 +153,13 @@ class InvenTreeSetting(models.Model): 'SALESORDER_REFERENCE_PREFIX': { 'name': _('Sales Order Reference Prefix'), 'description': _('Prefix value for sales order reference'), + 'default': 'SO', }, 'PURCHASEORDER_REFERENCE_PREFIX': { 'name': _('Purchase Order Reference Prefix'), 'description': _('Prefix value for purchase order reference'), + 'default': 'PO', }, } diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 4a9dbfa2ac..6b8a3c81e0 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -266,6 +266,17 @@ class SOList(generics.ListCreateAPIView): else: 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) if status is not None: diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index aa0c897c8a..d797a4e42d 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -128,6 +128,13 @@ class EditSalesOrderForm(HelperForm): super().__init__(*args, **kwargs) + # TODO: Improve this using a better date picker + target_date = forms.DateField( + widget=forms.DateInput( + attrs={'type': 'date'}, + ) + ) + class Meta: model = SalesOrder fields = [ @@ -135,6 +142,7 @@ class EditSalesOrderForm(HelperForm): 'customer', 'customer_reference', 'description', + 'target_date', 'link' ] diff --git a/InvenTree/order/migrations/0040_salesorder_target_date.py b/InvenTree/order/migrations/0040_salesorder_target_date.py new file mode 100644 index 0000000000..29a90a6ad5 --- /dev/null +++ b/InvenTree/order/migrations/0040_salesorder_target_date.py @@ -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'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index b2eb59c52e..9045ab86d7 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -9,7 +9,7 @@ from datetime import datetime from decimal import Decimal 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.core.validators import MinValueValidator from django.core.exceptions import ValidationError @@ -26,7 +26,7 @@ from stock import models as stock_models from company.models import Company, SupplierPart 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.models import InvenTreeAttachment @@ -49,8 +49,6 @@ class Order(models.Model): """ - ORDER_PREFIX = "" - @classmethod def getNextOrderNumber(cls): """ @@ -88,16 +86,6 @@ class Order(models.Model): 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() @@ -132,11 +120,12 @@ class PurchaseOrder(Order): supplier_reference: Optional field for supplier order reference code received_by: User that received the goods """ - - ORDER_PREFIX = "PO" def __str__(self): - return "PO {ref} - {company}".format(ref=self.reference, company=self.supplier.name) + + prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX') + + return f"{prefix}{self.reference} - {self.supplier.name}" status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(), help_text=_('Purchase order status')) @@ -307,10 +296,16 @@ class SalesOrder(Order): Attributes: customer: Reference to the company receiving the goods in the order 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): - 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): 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")) + 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) shipped_by = models.ForeignKey( @@ -338,6 +339,23 @@ class SalesOrder(Order): 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 def is_pending(self): return self.status == SalesOrderStatus.PENDING diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index c91ec6e02d..f0505728f6 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -9,6 +9,9 @@ from rest_framework import serializers 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 InvenTreeAttachmentSerializerField @@ -152,12 +155,24 @@ class SalesOrderSerializer(InvenTreeModelSerializer): def annotate_queryset(queryset): """ Add extra information to the queryset + + - Number of line items in the SalesOrder + - Overdue status of the SalesOrder """ queryset = queryset.annotate( 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 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) + overdue = serializers.BooleanField() + class Meta: model = SalesOrder fields = [ 'pk', - 'shipment_date', 'creation_date', - 'description', - 'line_items', - 'link', - 'reference', 'customer', 'customer_detail', 'customer_reference', + 'description', + 'line_items', + 'link', + 'notes', + 'overdue', + 'reference', 'status', 'status_text', 'shipment_date', - 'notes', + 'target_date', ] read_only_fields = [ diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 59ff0eeda8..378020850b 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -37,6 +37,9 @@ src="{% static 'img/blank_image.png' %}"
{{ order.description }}
@@ -74,7 +77,12 @@ src="{% static 'img/blank_image.png' %}"