diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index d820dfb716..279225355d 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -311,6 +311,14 @@ if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover INSTALLED_APPS.append('debug_toolbar') MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') +# InvenTree URL configuration + +# Base URL for admin pages (default="admin") +INVENTREE_ADMIN_URL = get_setting( + 'INVENTREE_ADMIN_URL', + CONFIG.get('admin_url', 'admin'), +) + ROOT_URLCONF = 'InvenTree.urls' TEMPLATES = [ diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b1941da8db..cf996547df 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -4,7 +4,6 @@ Top-level URL lookup for InvenTree application. Passes URL lookup downstream to each app as required. """ - from django.conf.urls import url, include from django.urls import path from django.contrib import admin @@ -169,9 +168,9 @@ frontendpatterns = [ url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), # admin sites - url(r'^admin/error_log/', include('error_report.urls')), - url(r'^admin/shell/', include('django_admin_shell.urls')), - url(r'^admin/', admin.site.urls, name='inventree-admin'), + url(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')), + url(f'^{settings.INVENTREE_ADMIN_URL}/shell/', include('django_admin_shell.urls')), + url(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'), # DB user sessions url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ), diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index b4314f6738..678f69d228 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,14 @@ import common.models INVENTREE_SW_VERSION = "0.7.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 26 +INVENTREE_API_VERSION = 27 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v27 -> 2022-02-28 + - Adds target_date field to individual line items for purchase orders and sales orders + v26 -> 2022-02-17 - Adds API endpoint for uploading a BOM file and extracting data diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 095a8cf70c..01c2c781e9 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -383,9 +383,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): Returns the BOM items for the part referenced by this BuildOrder """ - return self.part.bom_items.all().prefetch_related( - 'sub_part' - ) + return self.part.get_bom_items() @property def tracked_bom_items(self): diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index cd7126a801..4d2c77278c 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -151,7 +151,7 @@ src="{% static 'img/blank_image.png' %}" {% trans "Target Date" %}
@@ -120,19 +120,19 @@ |
{% trans "Created" %} | -{{ build.creation_date }} | +{% render_date build.creation_date %} | ||||||
{% trans "Target Date" %} | {% if build.target_date %}- {{ build.target_date }}{% if build.is_overdue %} {% endif %} + {% render_date build.target_date %}{% if build.is_overdue %} {% endif %} | {% else %}{% trans "No target date set" %} | @@ -142,7 +142,7 @@{% trans "Completed" %} | {% if build.completion_date %} -{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %} | +{% render_date build.completion_date %}{% if build.completed_by %}{{ build.completed_by }}{% endif %} | {% else %}{% trans "Build not complete" %} | {% endif %} diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 9ef5a4d0c3..fe6ddecbba 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -443,12 +443,12 @@ class BaseInvenTreeSetting(models.Model): except self.DoesNotExist: pass - def choices(self): + def choices(self, **kwargs): """ Return the available choices for this setting (or None if no choices are defined) """ - return self.__class__.get_setting_choices(self.key) + return self.__class__.get_setting_choices(self.key, **kwargs) def valid_options(self): """ @@ -462,6 +462,33 @@ class BaseInvenTreeSetting(models.Model): return [opt[0] for opt in choices] + def is_choice(self, **kwargs): + """ + Check if this setting is a "choice" field + """ + + return self.__class__.get_setting_choices(self.key, **kwargs) is not None + + def as_choice(self, **kwargs): + """ + Render this setting as the "display" value of a choice field, + e.g. if the choices are: + [('A4', 'A4 paper'), ('A3', 'A3 paper')], + and the value is 'A4', + then display 'A4 paper' + """ + + choices = self.get_setting_choices(self.key, **kwargs) + + if not choices: + return self.value + + for value, display in choices: + if value == self.value: + return display + + return self.value + def is_bool(self, **kwargs): """ Check if this setting is required to be a boolean value @@ -1212,6 +1239,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'default': False, 'validator': bool, }, + + 'DATE_DISPLAY_FORMAT': { + 'name': _('Date Format'), + 'description': _('Preferred format for displaying dates'), + 'default': 'YYYY-MM-DD', + 'choices': [ + ('YYYY-MM-DD', '2022-02-22'), + ('YYYY/MM/DD', '2022/22/22'), + ('DD-MM-YYYY', '22-02-2022'), + ('DD/MM/YYYY', '22/02/2022'), + ('MM-DD-YYYY', '02-22-2022'), + ('MM/DD/YYYY', '02/22/2022'), + ('MMM DD YYYY', 'Feb 22 2022'), + ] + } } class Meta: diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index b5622d7ce8..66c61bd0d7 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -274,7 +274,7 @@ class POLineItemFilter(rest_filters.FilterSet): model = models.PurchaseOrderLineItem fields = [ 'order', - 'part' + 'part', ] pending = rest_filters.BooleanFilter(label='pending', method='filter_pending') @@ -391,6 +391,7 @@ class POLineItemList(generics.ListCreateAPIView): 'reference', 'SKU', 'total_price', + 'target_date', ] search_fields = [ @@ -401,11 +402,6 @@ class POLineItemList(generics.ListCreateAPIView): 'reference', ] - filter_fields = [ - 'order', - 'part' - ] - class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView): """ @@ -703,6 +699,7 @@ class SOLineItemList(generics.ListCreateAPIView): 'part__name', 'quantity', 'reference', + 'target_date', ] search_fields = [ diff --git a/InvenTree/order/migrations/0062_auto_20220228_0321.py b/InvenTree/order/migrations/0062_auto_20220228_0321.py new file mode 100644 index 0000000000..7f67a827a2 --- /dev/null +++ b/InvenTree/order/migrations/0062_auto_20220228_0321.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.10 on 2022-02-28 03:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderlineitem', + name='target_date', + field=models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date'), + ), + migrations.AddField( + model_name='salesorderlineitem', + name='target_date', + field=models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date'), + ), + ] diff --git a/InvenTree/order/migrations/0063_alter_purchaseorderlineitem_unique_together.py b/InvenTree/order/migrations/0063_alter_purchaseorderlineitem_unique_together.py new file mode 100644 index 0000000000..1c73b6b437 --- /dev/null +++ b/InvenTree/order/migrations/0063_alter_purchaseorderlineitem_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.10 on 2022-02-28 04:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0062_auto_20220228_0321'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='purchaseorderlineitem', + unique_together=set(), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7f5c6b8164..f08880a882 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -816,9 +816,18 @@ class OrderLineItem(models.Model): Attributes: quantity: Number of items + reference: Reference text (e.g. customer reference) for this line item note: Annotation for the item + target_date: An (optional) date for expected shipment of this line item. + """ """ + Query filter for determining if an individual line item is "overdue": + - Amount received is less than the required quantity + - Target date is not None + - Target date is in the past + """ + OVERDUE_FILTER = Q(received__lt=F('quantity')) & ~Q(target_date=None) & Q(target_date__lt=datetime.now().date()) class Meta: abstract = True @@ -835,6 +844,12 @@ class OrderLineItem(models.Model): notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes')) + target_date = models.DateField( + blank=True, null=True, + verbose_name=_('Target Date'), + help_text=_('Target shipping date for this line item'), + ) + class PurchaseOrderLineItem(OrderLineItem): """ Model for a purchase order line item. @@ -846,7 +861,6 @@ class PurchaseOrderLineItem(OrderLineItem): class Meta: unique_together = ( - ('order', 'part', 'quantity', 'purchase_price') ) @staticmethod diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index cf8e701c68..2f4c1ea5df 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models, transaction from django.db.models import Case, When, Value -from django.db.models import BooleanField, ExpressionWrapper, F +from django.db.models import BooleanField, ExpressionWrapper, F, Q from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -28,7 +28,7 @@ from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import ReferenceIndexingSerializerMixin -from InvenTree.status_codes import StockStatus +from InvenTree.status_codes import StockStatus, PurchaseOrderStatus, SalesOrderStatus import order.models @@ -128,6 +128,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): Add some extra annotations to this queryset: - Total price = purchase_price * quantity + - "Overdue" status (boolean field) """ queryset = queryset.annotate( @@ -137,6 +138,15 @@ class POLineItemSerializer(InvenTreeModelSerializer): ) ) + queryset = queryset.annotate( + overdue=Case( + When( + Q(order__status__in=PurchaseOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()) + ), + default=Value(False, output_field=BooleanField()), + ) + ) + return queryset def __init__(self, *args, **kwargs): @@ -157,6 +167,8 @@ class POLineItemSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField(default=1) received = serializers.FloatField(default=0) + overdue = serializers.BooleanField(required=False, read_only=True) + total_price = serializers.FloatField(read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) @@ -187,6 +199,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'notes', 'order', 'order_detail', + 'overdue', 'part', 'part_detail', 'supplier_part_detail', @@ -196,6 +209,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'purchase_price_string', 'destination', 'destination_detail', + 'target_date', 'total_price', ] @@ -601,6 +615,23 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): class SOLineItemSerializer(InvenTreeModelSerializer): """ Serializer for a SalesOrderLineItem object """ + @staticmethod + def annotate_queryset(queryset): + """ + Add some extra annotations to this queryset: + + - "Overdue" status (boolean field) + """ + + queryset = queryset.annotate( + overdue=Case( + When( + Q(order__status__in=SalesOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()), + ) + ) + def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) @@ -622,6 +653,8 @@ class SOLineItemSerializer(InvenTreeModelSerializer): part_detail = PartBriefSerializer(source='part', many=False, read_only=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) + overdue = serializers.BooleanField(required=False, read_only=True) + quantity = InvenTreeDecimalField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) @@ -651,12 +684,14 @@ class SOLineItemSerializer(InvenTreeModelSerializer): 'notes', 'order', 'order_detail', + 'overdue', 'part', 'part_detail', 'sale_price', 'sale_price_currency', 'sale_price_string', 'shipped', + 'target_date', ] diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index af1e02fd54..c188e183d0 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -141,27 +141,27 @@ src="{% static 'img/blank_image.png' %}"||
{% trans "Created" %} | -{{ order.creation_date }}{{ order.created_by }} | +{% render_date order.creation_date %}{{ order.created_by }} | ||||||
{% trans "Issued" %} | -{{ order.issue_date }} | +{% render_date order.issue_date %} | ||||||
{% trans "Target Date" %} | -{{ order.target_date }} | +{% render_date order.target_date %} | ||||||
{% trans "Received" %} | -{{ order.complete_date }}{{ order.received_by }} | +{% render_date order.complete_date %}{{ order.received_by }} | ||||||
{% trans "Created" %} | -{{ order.creation_date }}{{ order.created_by }} | +{% render_date order.creation_date %}{{ order.created_by }} | ||||||
{% trans "Target Date" %} | -{{ order.target_date }} | +{% render_date order.target_date %} | {% trans "Completed" %} |
- {{ order.shipment_date }}
+ {% render_date order.shipment_date %}
{% if order.shipped_by %}
{{ order.shipped_by }}
{% endif %}
diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html
index 48b2542752..3676268f5c 100644
--- a/InvenTree/order/templates/order/sales_order_detail.html
+++ b/InvenTree/order/templates/order/sales_order_detail.html
@@ -238,6 +238,7 @@
reference: {},
sale_price: {},
sale_price_currency: {},
+ target_date: {},
notes: {},
},
method: 'POST',
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 33ad8bf612..09e1f77542 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -1453,7 +1453,9 @@ class Part(MPTTModel):
By default, will include inherited BOM items
"""
- return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
+ queryset = BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
+
+ return queryset.prefetch_related('sub_part')
def get_installed_part_options(self, include_inherited=True, include_variants=True):
"""
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 2266b39048..a6cfda757f 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -122,7 +122,13 @@
{% trans "Sales Order Allocations" %}
-
@@ -342,7 +348,12 @@
{% trans "Build Order Allocations" %}
-
@@ -722,6 +733,7 @@
});
// Load the BOM table data in the pricing view
+ {% if part.has_bom and roles.sales_order.view %}
loadBomTable($("#bom-pricing-table"), {
editable: false,
bom_url: "{% url 'api-bom-list' %}",
@@ -729,6 +741,7 @@
parent_id: {{ part.id }} ,
sub_part_detail: true,
});
+ {% endif %}
onPanelLoad("purchase-orders", function() {
loadPartPurchaseOrderTable(
@@ -952,7 +965,7 @@
{% if price_history %}
var purchasepricedata = {
labels: [
- {% for line in price_history %}'{{ line.date }}',{% endfor %}
+ {% for line in price_history %}'{% render_date line.date %}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
@@ -1065,7 +1078,7 @@
{% if sale_history %}
var salepricedata = {
labels: [
- {% for line in sale_history %}'{{ line.date }}',{% endfor %}
+ {% for line in sale_history %}'{% render_date line.date %}',{% endfor %}
],
datasets: [{
label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index d2505a57f7..fe18c68a38 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -59,13 +59,13 @@
|