mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
7050b3a410
@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
@ -133,10 +121,11 @@ class PurchaseOrder(Order):
|
||||
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)
|
||||
|
||||
|
@ -114,7 +114,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
<!-- Stock adjustment menu -->
|
||||
{% 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 %}
|
||||
|
@ -29,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>
|
||||
|
||||
@ -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() {
|
||||
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-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 %}
|
@ -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',
|
||||
|
@ -217,6 +217,10 @@ function getAvailableTableFilters(tableKey) {
|
||||
type: 'bool',
|
||||
title: '{% trans "Outstanding" %}',
|
||||
},
|
||||
overdue: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Overdue" %}',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user