mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merging master and resolved conflict
This commit is contained in:
commit
3ff76fbdab
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
18
InvenTree/build/migrations/0025_build_target_date.py
Normal file
18
InvenTree/build/migrations/0025_build_target_date.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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):
|
||||
"""
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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>
|
||||
{% endif %}
|
||||
</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>
|
||||
<p>{{ build.title }}</p>
|
||||
<div class='btn-row'>
|
||||
@ -81,7 +86,12 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></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>
|
||||
<td><span class='fas fa-spinner'></span></td>
|
||||
|
@ -95,33 +95,26 @@
|
||||
<td>{% trans "Created" %}</td>
|
||||
<td>{{ build.creation_date }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<table class='table table-striped'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-dollar-sign'></span></td>
|
||||
<td>{% trans "BOM Price" %}</td>
|
||||
<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>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Target Date" %}</td>
|
||||
{% if build.target_date %}
|
||||
<td>
|
||||
{{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td><i>{% trans "No target date set" %}</i></td>
|
||||
{% 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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -721,7 +721,7 @@ class BuildUpdate(AjaxUpdateView):
|
||||
model = Build
|
||||
form_class = forms.EditBuildForm
|
||||
context_object_name = 'build'
|
||||
ajax_form_title = _('Edit Build Details')
|
||||
ajax_form_title = _('Edit Build Order Details')
|
||||
ajax_template_name = 'modal_form.html'
|
||||
role_required = 'build.change'
|
||||
|
||||
@ -764,7 +764,7 @@ class BuildDelete(AjaxDeleteView):
|
||||
|
||||
model = Build
|
||||
ajax_template_name = 'build/delete_build.html'
|
||||
ajax_form_title = _('Delete Build')
|
||||
ajax_form_title = _('Delete Build Order')
|
||||
role_required = 'build.delete'
|
||||
|
||||
|
||||
|
@ -160,11 +160,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',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -3,3 +3,6 @@ import multiprocessing
|
||||
bind = "0.0.0.0:8000"
|
||||
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
|
||||
max_requests = 1000
|
||||
max_requests_jitter = 50
|
||||
|
Binary file not shown.
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
@ -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:
|
||||
|
@ -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'
|
||||
]
|
||||
|
||||
|
18
InvenTree/order/migrations/0040_salesorder_target_date.py
Normal file
18
InvenTree/order/migrations/0040_salesorder_target_date.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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 = [
|
||||
|
@ -37,6 +37,9 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</h3>
|
||||
<h3>
|
||||
{% 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>
|
||||
<hr>
|
||||
<p>{{ order.description }}</p>
|
||||
@ -74,7 +77,12 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></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>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
@ -100,6 +108,13 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{% trans "Created" %}</td>
|
||||
<td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td>
|
||||
</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 %}
|
||||
<tr>
|
||||
<td><span class='fas fa-truck'></span></td>
|
||||
|
@ -31,11 +31,11 @@ class OrderTest(TestCase):
|
||||
|
||||
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
|
||||
|
||||
self.assertEqual(str(order), 'PO 0001 - ACME')
|
||||
self.assertEqual(str(order), 'PO0001 - ACME')
|
||||
|
||||
line = PurchaseOrderLineItem.objects.get(pk=1)
|
||||
|
||||
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 0001 - ACME)")
|
||||
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
|
||||
|
||||
def test_increment(self):
|
||||
|
||||
|
@ -130,7 +130,7 @@ class CategoryParameters(generics.ListAPIView):
|
||||
"""
|
||||
|
||||
try:
|
||||
cat_id = int(self.request.query_params.get('category', None))
|
||||
cat_id = int(self.kwargs.get('pk', None))
|
||||
except TypeError:
|
||||
cat_id = None
|
||||
fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true'))
|
||||
@ -910,8 +910,8 @@ part_api_urls = [
|
||||
|
||||
# Base URL for PartCategory API endpoints
|
||||
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'^parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
|
||||
url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
|
||||
])),
|
||||
|
||||
|
@ -594,7 +594,7 @@ class Part(MPTTModel):
|
||||
# User can decide whether duplicate IPN (Internal Part Number) values are allowed
|
||||
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 = parts.exclude(pk=self.pk)
|
||||
|
||||
|
@ -193,6 +193,9 @@ class TestReport(ReportTemplateBase):
|
||||
|
||||
items = StockItem.objects.filter(**filters)
|
||||
|
||||
# Ensure the provided StockItem object matches the filters
|
||||
items = items.filter(pk=item.pk)
|
||||
|
||||
return items.exists()
|
||||
|
||||
def get_context_data(self, request):
|
||||
|
@ -127,7 +127,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
{% if owner_control.value == "False" or owner_control.value == "True" and item.owner == user or user.is_superuser %}
|
||||
{% if roles.stock.change and not item.is_building %}
|
||||
<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'>
|
||||
{% if item.in_stock %}
|
||||
{% if not item.serialized %}
|
||||
|
@ -538,7 +538,22 @@ class StockItemTestReportSelect(AjaxView):
|
||||
def get_form(self):
|
||||
|
||||
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
|
||||
return StockForms.TestReportFormatForm(stock_item)
|
||||
form = StockForms.TestReportFormatForm(stock_item)
|
||||
|
||||
return form
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
form = self.get_form()
|
||||
options = form.fields['template'].queryset
|
||||
|
||||
# If only a single template is available, pre-select it
|
||||
if options.count() == 1:
|
||||
initials['template'] = options[0]
|
||||
|
||||
return initials
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
@ -1320,6 +1335,7 @@ class StockItemEdit(AjaxUpdateView):
|
||||
# If the part cannot be purchased, hide the supplier_part field
|
||||
if not item.part.purchaseable:
|
||||
form.fields['supplier_part'].widget = HiddenInput()
|
||||
form.fields['purchase_price'].widget = HiddenInput()
|
||||
else:
|
||||
query = form.fields['supplier_part'].queryset
|
||||
query = query.filter(part=item.part.id)
|
||||
@ -1622,6 +1638,9 @@ class StockItemCreate(AjaxCreateView):
|
||||
|
||||
form.rebuild_layout()
|
||||
|
||||
if not part.purchaseable:
|
||||
form.fields['purchase_price'].widget = HiddenInput()
|
||||
|
||||
# Hide the 'part' field (as a valid part is selected)
|
||||
# form.fields['part'].widget = HiddenInput()
|
||||
|
||||
|
15
InvenTree/templates/InvenTree/build_overdue.html
Normal file
15
InvenTree/templates/InvenTree/build_overdue.html
Normal 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 %}
|
@ -16,6 +16,7 @@ InvenTree | {% trans "Index" %}
|
||||
{% endif %}
|
||||
{% if roles.build.view %}
|
||||
{% include "InvenTree/build_pending.html" with collapse_id="build_pending" %}
|
||||
{% include "InvenTree/build_overdue.html" with collapse_id="build_overdue" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
@ -28,6 +29,7 @@ InvenTree | {% trans "Index" %}
|
||||
{% endif %}
|
||||
{% if roles.sales_order.view %}
|
||||
{% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %}
|
||||
{% include "InvenTree/so_overdue.html" with collapse_id="so_overdue" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -72,6 +74,15 @@ loadBuildTable("#build-pending-table", {
|
||||
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' %}", {
|
||||
params: {
|
||||
low_stock: true,
|
||||
@ -102,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() {
|
||||
var count = $("#latest-parts-table").bootstrapTable('getData').length;
|
||||
|
||||
@ -126,6 +145,12 @@ $("#build-pending-table").on('load-success.bs.table', function() {
|
||||
$("#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() {
|
||||
var count = $("#low-stock-table").bootstrapTable('getData').length;
|
||||
|
||||
@ -150,4 +175,10 @@ $("#so-outstanding-table").on('load-success.bs.table', function() {
|
||||
$("#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 %}
|
@ -45,7 +45,7 @@
|
||||
|
||||
{% if category %}
|
||||
$("#param-table").inventreeTable({
|
||||
url: "{% url 'api-part-category-parameters' category.pk %}",
|
||||
url: "{% url 'api-part-category-parameters' pk=category.pk %}",
|
||||
queryParams: {
|
||||
ordering: 'name',
|
||||
},
|
||||
@ -58,7 +58,7 @@
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'parameter_template_detail.name',
|
||||
field: 'parameter_template.name',
|
||||
title: '{% trans "Parameter Template" %}',
|
||||
sortable: 'true',
|
||||
},
|
||||
|
15
InvenTree/templates/InvenTree/so_overdue.html
Normal file
15
InvenTree/templates/InvenTree/so_overdue.html
Normal 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 %}
|
@ -650,7 +650,13 @@ function loadBuildTable(table, options) {
|
||||
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" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'target_date',
|
||||
title: '{% trans "Target Date" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'completion_date',
|
||||
title: '{% trans "Completed" %}',
|
||||
|
@ -235,7 +235,13 @@ function loadSalesOrderTable(table, options) {
|
||||
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',
|
||||
title: '{% trans "Creation Date" %}',
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'target_date',
|
||||
title: '{% trans "Target Date" %}',
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'shipment_date',
|
||||
|
@ -202,7 +202,7 @@ function loadPartVariantTable(table, partId, options={}) {
|
||||
showColumns: true,
|
||||
original: params,
|
||||
queryParams: filters,
|
||||
formatNoMatches: function() { return "{% trans "No variants found" %}"; },
|
||||
formatNoMatches: function() { return '{% trans "No variants found" %}'; },
|
||||
columns: cols,
|
||||
treeEnable: true,
|
||||
rootParentId: partId,
|
||||
@ -249,7 +249,7 @@ function loadParametricPartTable(table, options={}) {
|
||||
if (header === 'part') {
|
||||
columns.push({
|
||||
field: header,
|
||||
title: '{% trans 'Part' %}',
|
||||
title: '{% trans "Part" %}',
|
||||
sortable: true,
|
||||
sortName: 'name',
|
||||
formatter: function(value, row, index, field) {
|
||||
@ -268,7 +268,7 @@ function loadParametricPartTable(table, options={}) {
|
||||
} else if (header === 'description') {
|
||||
columns.push({
|
||||
field: header,
|
||||
title: '{% trans 'Description' %}',
|
||||
title: '{% trans "Description" %}',
|
||||
sortable: true,
|
||||
});
|
||||
} else {
|
||||
@ -288,7 +288,7 @@ function loadParametricPartTable(table, options={}) {
|
||||
queryParams: table_headers,
|
||||
groupBy: false,
|
||||
name: options.name || 'parametric',
|
||||
formatNoMatches: function() { return "{% trans "No parts found" %}"; },
|
||||
formatNoMatches: function() { return '{% trans "No parts found" %}'; },
|
||||
columns: columns,
|
||||
showColumns: true,
|
||||
data: table_data,
|
||||
@ -454,7 +454,7 @@ function loadPartTable(table, url, options={}) {
|
||||
groupBy: false,
|
||||
name: options.name || 'part',
|
||||
original: params,
|
||||
formatNoMatches: function() { return "{% trans "No parts found" %}"; },
|
||||
formatNoMatches: function() { return '{% trans "No parts found" %}'; },
|
||||
columns: columns,
|
||||
showColumns: true,
|
||||
});
|
||||
@ -564,12 +564,12 @@ function loadPartTestTemplateTable(table, options) {
|
||||
},
|
||||
{
|
||||
field: 'test_name',
|
||||
title: "{% trans "Test Name" %}",
|
||||
title: '{% trans "Test Name" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: "{% trans "Description" %}",
|
||||
title: '{% trans "Description" %}',
|
||||
},
|
||||
{
|
||||
field: 'required',
|
||||
@ -581,14 +581,14 @@ function loadPartTestTemplateTable(table, options) {
|
||||
},
|
||||
{
|
||||
field: 'requires_value',
|
||||
title: "{% trans "Requires Value" %}",
|
||||
title: '{% trans "Requires Value" %}',
|
||||
formatter: function(value) {
|
||||
return yesNoLabel(value);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'requires_attachment',
|
||||
title: "{% trans "Requires Attachment" %}",
|
||||
title: '{% trans "Requires Attachment" %}',
|
||||
formatter: function(value) {
|
||||
return yesNoLabel(value);
|
||||
}
|
||||
@ -608,7 +608,9 @@ function loadPartTestTemplateTable(table, options) {
|
||||
|
||||
return html;
|
||||
} 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/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,7 +184,11 @@ function getAvailableTableFilters(tableKey) {
|
||||
active: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Active" %}',
|
||||
}
|
||||
},
|
||||
overdue: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Overdue" %}',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -213,6 +217,10 @@ function getAvailableTableFilters(tableKey) {
|
||||
type: 'bool',
|
||||
title: '{% trans "Outstanding" %}',
|
||||
},
|
||||
overdue: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Overdue" %}',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@ -26,22 +27,25 @@
|
||||
|
||||
<div class='login'>
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-md-offset-5">
|
||||
<div class='container-fluid'>
|
||||
<div class='clearfix content-heading'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/> <h3>InvenTree</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/> <h3>InvenTree</h3>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class='container-fluid'>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class='pull-right btn btn-primary' type="submit">Login</button>
|
||||
</form>
|
||||
<div class='container-fluid'>
|
||||
<form method="post" action=''>
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{{ form|crispy }}
|
||||
|
||||
<button class='pull-right btn btn-primary' type="submit">{% trans "Login" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -87,6 +87,64 @@ class RoleGroupAdmin(admin.ModelAdmin):
|
||||
RuleSetInline,
|
||||
]
|
||||
|
||||
list_display = ('name', 'admin', 'part', 'stock', 'build', 'purchase_order', 'sales_order')
|
||||
|
||||
def get_rule_set(self, obj, rule_set_type):
|
||||
''' Return list of permissions for the given ruleset '''
|
||||
|
||||
# Get all rulesets associated to object
|
||||
rule_sets = RuleSet.objects.filter(group=obj.pk)
|
||||
|
||||
# Select ruleset based on type
|
||||
for rule_set in rule_sets:
|
||||
if rule_set.name == rule_set_type:
|
||||
break
|
||||
|
||||
def append_permission_level(permission_level, next_level):
|
||||
if not permission_level:
|
||||
return next_level
|
||||
|
||||
if permission_level[:-1].endswith('|'):
|
||||
permission_level += next_level
|
||||
else:
|
||||
permission_level += ' | ' + next_level
|
||||
|
||||
return permission_level
|
||||
|
||||
permission_level = ''
|
||||
|
||||
if rule_set.can_view:
|
||||
permission_level = append_permission_level(permission_level, 'V')
|
||||
|
||||
if rule_set.can_add:
|
||||
permission_level = append_permission_level(permission_level, 'A')
|
||||
|
||||
if rule_set.can_change:
|
||||
permission_level = append_permission_level(permission_level, 'C')
|
||||
|
||||
if rule_set.can_delete:
|
||||
permission_level = append_permission_level(permission_level, 'D')
|
||||
|
||||
return permission_level
|
||||
|
||||
def admin(self, obj):
|
||||
return self.get_rule_set(obj, 'admin')
|
||||
|
||||
def part(self, obj):
|
||||
return self.get_rule_set(obj, 'part')
|
||||
|
||||
def stock(self, obj):
|
||||
return self.get_rule_set(obj, 'stock')
|
||||
|
||||
def build(self, obj):
|
||||
return self.get_rule_set(obj, 'build')
|
||||
|
||||
def purchase_order(self, obj):
|
||||
return self.get_rule_set(obj, 'purchase_order')
|
||||
|
||||
def sales_order(self, obj):
|
||||
return self.get_rule_set(obj, 'sales_order')
|
||||
|
||||
def get_formsets_with_inlines(self, request, obj=None):
|
||||
for inline in self.get_inline_instances(request, obj):
|
||||
# Hide RuleSetInline in the 'Add role' view
|
||||
|
Loading…
Reference in New Issue
Block a user