mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-05 01:03:37 +00:00
@@ -274,7 +274,9 @@ def send_email(subject, body, recipients, from_email=None):
|
|||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
'django.core.mail.send_mail',
|
'django.core.mail.send_mail',
|
||||||
subject, body,
|
subject,
|
||||||
|
body,
|
||||||
from_email,
|
from_email,
|
||||||
recipients,
|
recipients,
|
||||||
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import re
|
|||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.5.0"
|
INVENTREE_SW_VERSION = "0.5.1"
|
||||||
|
|
||||||
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 12
|
INVENTREE_API_VERSION = 12
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -96,16 +97,14 @@ def inventreeDocsVersion():
|
|||||||
Return the version string matching the latest documentation.
|
Return the version string matching the latest documentation.
|
||||||
|
|
||||||
Development -> "latest"
|
Development -> "latest"
|
||||||
Release -> "major.minor"
|
Release -> "major.minor.sub" e.g. "0.5.2"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isInvenTreeDevelopmentVersion():
|
if isInvenTreeDevelopmentVersion():
|
||||||
return "latest"
|
return "latest"
|
||||||
else:
|
else:
|
||||||
major, minor, patch = inventreeVersionTuple()
|
return INVENTREE_SW_VERSION
|
||||||
|
|
||||||
return f"{major}.{minor}"
|
|
||||||
|
|
||||||
|
|
||||||
def isInvenTreeUpToDate():
|
def isInvenTreeUpToDate():
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ loadStockTable($("#build-stock-table"), {
|
|||||||
location_detail: true,
|
location_detail: true,
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
build: {{ build.id }},
|
build: {{ build.id }},
|
||||||
|
is_building: false,
|
||||||
},
|
},
|
||||||
groupByField: 'location',
|
groupByField: 'location',
|
||||||
buttons: [
|
buttons: [
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-10-04 20:41
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('company', '0040_alter_company_currency'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='company',
|
||||||
|
options={'ordering': ['name'], 'verbose_name_plural': 'Companies'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -94,6 +94,7 @@ class Company(models.Model):
|
|||||||
constraints = [
|
constraints = [
|
||||||
UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair')
|
UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair')
|
||||||
]
|
]
|
||||||
|
verbose_name_plural = "Companies"
|
||||||
|
|
||||||
name = models.CharField(max_length=100, blank=False,
|
name = models.CharField(max_length=100, blank=False,
|
||||||
help_text=_('Company name'),
|
help_text=_('Company name'),
|
||||||
|
|||||||
+1525
-1356
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1656
-1483
File diff suppressed because it is too large
Load Diff
+1491
-1324
File diff suppressed because it is too large
Load Diff
+1490
-1323
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1491
-1324
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1493
-1326
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1493
-1326
File diff suppressed because it is too large
Load Diff
+1489
-1322
File diff suppressed because it is too large
Load Diff
+1495
-1328
File diff suppressed because it is too large
Load Diff
+14
-6
@@ -8,11 +8,13 @@ from __future__ import unicode_literals
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
|
||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework import filters, status
|
from rest_framework import filters, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
|
|
||||||
@@ -243,11 +245,12 @@ class POReceive(generics.CreateAPIView):
|
|||||||
|
|
||||||
pk = self.kwargs.get('pk', None)
|
pk = self.kwargs.get('pk', None)
|
||||||
|
|
||||||
if pk is None:
|
try:
|
||||||
return None
|
order = PurchaseOrder.objects.get(pk=pk)
|
||||||
else:
|
except (PurchaseOrder.DoesNotExist, ValueError):
|
||||||
order = PurchaseOrder.objects.get(pk=self.kwargs['pk'])
|
raise ValidationError(_("Matching purchase order does not exist"))
|
||||||
return order
|
|
||||||
|
return order
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
|
|
||||||
@@ -259,9 +262,14 @@ class POReceive(generics.CreateAPIView):
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
# Receive the line items
|
# Receive the line items
|
||||||
self.receive_items(serializer)
|
try:
|
||||||
|
self.receive_items(serializer)
|
||||||
|
except DjangoValidationError as exc:
|
||||||
|
# Re-throw a django error as a DRF error
|
||||||
|
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||||
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
|||||||
@@ -418,16 +418,24 @@ class PurchaseOrder(Order):
|
|||||||
barcode = ''
|
barcode = ''
|
||||||
|
|
||||||
if not self.status == PurchaseOrderStatus.PLACED:
|
if not self.status == PurchaseOrderStatus.PLACED:
|
||||||
raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})
|
raise ValidationError(
|
||||||
|
"Lines can only be received against an order marked as 'PLACED'"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not (quantity % 1 == 0):
|
if not (quantity % 1 == 0):
|
||||||
raise ValidationError({"quantity": _("Quantity must be an integer")})
|
raise ValidationError({
|
||||||
|
"quantity": _("Quantity must be an integer")
|
||||||
|
})
|
||||||
if quantity < 0:
|
if quantity < 0:
|
||||||
raise ValidationError({"quantity": _("Quantity must be a positive number")})
|
raise ValidationError({
|
||||||
|
"quantity": _("Quantity must be a positive number")
|
||||||
|
})
|
||||||
quantity = int(quantity)
|
quantity = int(quantity)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
raise ValidationError({"quantity": _("Invalid quantity provided")})
|
raise ValidationError({
|
||||||
|
"quantity": _("Invalid quantity provided")
|
||||||
|
})
|
||||||
|
|
||||||
# Create a new stock item
|
# Create a new stock item
|
||||||
if line.part and quantity > 0:
|
if line.part and quantity > 0:
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
help_text=_('Unique identifier field'),
|
help_text=_('Unique identifier field'),
|
||||||
default='',
|
default='',
|
||||||
required=False,
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_barcode(self, barcode):
|
def validate_barcode(self, barcode):
|
||||||
@@ -494,7 +495,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
allocations = SalesOrderAllocationSerializer(many=True, read_only=True)
|
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
|
|||||||
@@ -158,467 +158,38 @@
|
|||||||
$("#so-lines-table").bootstrapTable("refresh");
|
$("#so-lines-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#new-so-line").click(function() {
|
$("#new-so-line").click(function() {
|
||||||
|
|
||||||
constructForm('{% url "api-so-line-list" %}', {
|
constructForm('{% url "api-so-line-list" %}', {
|
||||||
fields: {
|
|
||||||
order: {
|
|
||||||
value: {{ order.pk }},
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
part: {},
|
|
||||||
quantity: {},
|
|
||||||
reference: {},
|
|
||||||
sale_price: {},
|
|
||||||
sale_price_currency: {},
|
|
||||||
notes: {},
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
title: '{% trans "Add Line Item" %}',
|
|
||||||
onSuccess: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
function showAllocationSubTable(index, row, element) {
|
|
||||||
// Construct a table showing stock items which have been allocated against this line item
|
|
||||||
|
|
||||||
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`;
|
|
||||||
|
|
||||||
element.html(html);
|
|
||||||
|
|
||||||
var lineItem = row;
|
|
||||||
|
|
||||||
var table = $(`#allocation-table-${row.pk}`);
|
|
||||||
|
|
||||||
table.bootstrapTable({
|
|
||||||
data: row.allocations,
|
|
||||||
showHeader: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
width: '50%',
|
|
||||||
field: 'allocated',
|
|
||||||
title: '{% trans "Quantity" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var text = '';
|
|
||||||
|
|
||||||
if (row.serial != null && row.quantity == 1) {
|
|
||||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
|
||||||
} else {
|
|
||||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderLink(text, `/stock/item/${row.item}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'location',
|
|
||||||
title: 'Location',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return renderLink(row.location_path, `/stock/location/${row.location}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'po'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'buttons',
|
|
||||||
title: '{% trans "Actions" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var html = "<div class='btn-group float-right' role='group'>";
|
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
|
||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
html += "</div>";
|
|
||||||
|
|
||||||
return html;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-allocation-edit").click(function() {
|
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-allocation-delete").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
function showFulfilledSubTable(index, row, element) {
|
|
||||||
// Construct a table showing stock items which have been fulfilled against this line item
|
|
||||||
|
|
||||||
var id = `fulfilled-table-${row.pk}`;
|
|
||||||
var html = `<div class='sub-table'><table class='table table-striped table-condensed' id='${id}'></table></div>`;
|
|
||||||
|
|
||||||
element.html(html);
|
|
||||||
|
|
||||||
var lineItem = row;
|
|
||||||
|
|
||||||
$(`#${id}`).bootstrapTable({
|
|
||||||
url: "{% url 'api-stock-list' %}",
|
|
||||||
queryParams: {
|
|
||||||
part: row.part,
|
|
||||||
sales_order: {{ order.id }},
|
|
||||||
},
|
|
||||||
showHeader: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'stock',
|
|
||||||
formatter: function(value, row) {
|
|
||||||
var text = '';
|
|
||||||
if (row.serial && row.quantity == 1) {
|
|
||||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
|
||||||
} else {
|
|
||||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderLink(text, `/stock/item/${row.pk}/`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'po'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#so-lines-table").inventreeTable({
|
|
||||||
formatNoMatches: function() { return "{% trans 'No matching line items' %}"; },
|
|
||||||
queryParams: {
|
|
||||||
order: {{ order.id }},
|
|
||||||
part_detail: true,
|
|
||||||
allocations: true,
|
|
||||||
},
|
|
||||||
sidePagination: 'server',
|
|
||||||
uniqueId: 'pk',
|
|
||||||
url: "{% url 'api-so-line-list' %}",
|
|
||||||
onPostBody: setupCallbacks,
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING or order.status == SalesOrderStatus.SHIPPED %}
|
|
||||||
detailViewByClick: true,
|
|
||||||
detailView: true,
|
|
||||||
detailFilter: function(index, row) {
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
return row.allocated > 0;
|
|
||||||
{% else %}
|
|
||||||
return row.fulfilled > 0;
|
|
||||||
{% endif %}
|
|
||||||
},
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
detailFormatter: showAllocationSubTable,
|
|
||||||
{% else %}
|
|
||||||
detailFormatter: showFulfilledSubTable,
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
showFooter: true,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: '{% trans "ID" %}',
|
|
||||||
visible: false,
|
|
||||||
switchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
sortName: 'part__name',
|
|
||||||
field: 'part',
|
|
||||||
title: '{% trans "Part" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
if (row.part) {
|
|
||||||
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
|
|
||||||
} else {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
footerFormatter: function() {
|
|
||||||
return '{% trans "Total" %}'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'reference',
|
|
||||||
title: '{% trans "Reference" %}'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'quantity',
|
|
||||||
title: '{% trans "Quantity" %}',
|
|
||||||
footerFormatter: function(data) {
|
|
||||||
return data.map(function (row) {
|
|
||||||
return +row['quantity']
|
|
||||||
}).reduce(function (sum, i) {
|
|
||||||
return sum + i
|
|
||||||
}, 0)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
field: 'sale_price',
|
|
||||||
title: '{% trans "Unit Price" %}',
|
|
||||||
formatter: function(value, row) {
|
|
||||||
return row.sale_price_string || row.sale_price;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sortable: true,
|
|
||||||
title: '{% trans "Total price" %}',
|
|
||||||
formatter: function(value, row) {
|
|
||||||
var total = row.sale_price * row.quantity;
|
|
||||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.sale_price_currency});
|
|
||||||
return formatter.format(total)
|
|
||||||
},
|
|
||||||
footerFormatter: function(data) {
|
|
||||||
var total = data.map(function (row) {
|
|
||||||
return +row['sale_price']*row['quantity']
|
|
||||||
}).reduce(function (sum, i) {
|
|
||||||
return sum + i
|
|
||||||
}, 0)
|
|
||||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD';
|
|
||||||
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
|
|
||||||
return formatter.format(total)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'allocated',
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
title: '{% trans "Allocated" %}',
|
|
||||||
{% else %}
|
|
||||||
title: '{% trans "Fulfilled" %}',
|
|
||||||
{% endif %}
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
var quantity = row.allocated;
|
|
||||||
{% else %}
|
|
||||||
var quantity = row.fulfilled;
|
|
||||||
{% endif %}
|
|
||||||
return makeProgressBar(quantity, row.quantity, {
|
|
||||||
id: `order-line-progress-${row.pk}`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
sorter: function(valA, valB, rowA, rowB) {
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
var A = rowA.allocated;
|
|
||||||
var B = rowB.allocated;
|
|
||||||
{% else %}
|
|
||||||
var A = rowA.fulfilled;
|
|
||||||
var B = rowB.fulfilled;
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
if (A == 0 && B == 0) {
|
|
||||||
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var progressA = parseFloat(A) / rowA.quantity;
|
|
||||||
var progressB = parseFloat(B) / rowB.quantity;
|
|
||||||
|
|
||||||
return (progressA < progressB) ? 1 : -1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'notes',
|
|
||||||
title: '{% trans "Notes" %}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'po',
|
|
||||||
title: '{% trans "PO" %}',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var po_name = "";
|
|
||||||
if (row.allocated) {
|
|
||||||
row.allocations.forEach(function(allocation) {
|
|
||||||
if (allocation.po != po_name) {
|
|
||||||
if (po_name) {
|
|
||||||
po_name = "-";
|
|
||||||
} else {
|
|
||||||
po_name = allocation.po
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return `<div>` + po_name + `</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
|
||||||
{
|
|
||||||
field: 'buttons',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var html = `<div class='btn-group float-right' role='group'>`;
|
|
||||||
|
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
if (row.part) {
|
|
||||||
var part = row.part_detail;
|
|
||||||
|
|
||||||
if (part.trackable) {
|
|
||||||
html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
|
|
||||||
|
|
||||||
if (part.purchaseable) {
|
|
||||||
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.assembly) {
|
|
||||||
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
|
|
||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');
|
|
||||||
|
|
||||||
html += `</div>`;
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{% endif %}
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupCallbacks() {
|
|
||||||
|
|
||||||
var table = $("#so-lines-table");
|
|
||||||
|
|
||||||
// Set up callbacks for the row buttons
|
|
||||||
table.find(".button-edit").click(function() {
|
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
constructForm(`/api/order/so-line/${pk}/`, {
|
|
||||||
fields: {
|
fields: {
|
||||||
|
order: {
|
||||||
|
value: {{ order.pk }},
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
part: {},
|
||||||
quantity: {},
|
quantity: {},
|
||||||
reference: {},
|
reference: {},
|
||||||
sale_price: {},
|
sale_price: {},
|
||||||
sale_price_currency: {},
|
sale_price_currency: {},
|
||||||
notes: {},
|
notes: {},
|
||||||
},
|
},
|
||||||
title: '{% trans "Edit Line Item" %}',
|
method: 'POST',
|
||||||
|
title: '{% trans "Add Line Item" %}',
|
||||||
onSuccess: reloadTable,
|
onSuccess: reloadTable,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
table.find(".button-delete").click(function() {
|
loadSalesOrderLineItemTable(
|
||||||
var pk = $(this).attr('pk');
|
'#so-lines-table',
|
||||||
|
{
|
||||||
constructForm(`/api/order/so-line/${pk}/`, {
|
order: {{ order.pk }},
|
||||||
method: 'DELETE',
|
status: {{ order.status }},
|
||||||
title: '{% trans "Delete Line Item" %}',
|
|
||||||
onSuccess: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-add-by-sn").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
inventreeGet(`/api/order/so-line/${pk}/`, {},
|
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
launchModalForm('{% url "so-assign-serials" %}', {
|
|
||||||
success: reloadTable,
|
|
||||||
data: {
|
|
||||||
line: pk,
|
|
||||||
part: response.part,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-add").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(`/order/sales-order/allocation/new/`, {
|
|
||||||
success: reloadTable,
|
|
||||||
data: {
|
|
||||||
line: pk,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-build").click(function() {
|
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
// Extract the row data from the table!
|
|
||||||
var idx = $(this).closest('tr').attr('data-index');
|
|
||||||
|
|
||||||
var row = table.bootstrapTable('getData')[idx];
|
|
||||||
|
|
||||||
var quantity = 1;
|
|
||||||
|
|
||||||
if (row.allocated < row.quantity) {
|
|
||||||
quantity = row.quantity - row.allocated;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
launchModalForm(`/build/new/`, {
|
|
||||||
follow: true,
|
|
||||||
data: {
|
|
||||||
part: pk,
|
|
||||||
sales_order: {{ order.id }},
|
|
||||||
quantity: quantity,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
table.find(".button-buy").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm("{% url 'order-parts' %}", {
|
|
||||||
data: {
|
|
||||||
parts: [pk],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".button-price").click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
var idx = $(this).closest('tr').attr('data-index');
|
|
||||||
var row = table.bootstrapTable('getData')[idx];
|
|
||||||
|
|
||||||
launchModalForm(
|
|
||||||
"{% url 'line-pricing' %}",
|
|
||||||
{
|
|
||||||
submit_text: '{% trans "Calculate price" %}',
|
|
||||||
data: {
|
|
||||||
line_item: pk,
|
|
||||||
quantity: row.quantity,
|
|
||||||
},
|
|
||||||
buttons: [{name: 'update_price',
|
|
||||||
title: '{% trans "Update Unit Price" %}'},],
|
|
||||||
success: reloadTable,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
attachNavCallbacks({
|
attachNavCallbacks({
|
||||||
name: 'sales-order',
|
name: 'sales-order',
|
||||||
default: 'order-items'
|
default: 'order-items'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+36
-16
@@ -401,25 +401,45 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
self.assertEqual(line_1.received, 0)
|
self.assertEqual(line_1.received, 0)
|
||||||
self.assertEqual(line_2.received, 50)
|
self.assertEqual(line_2.received, 50)
|
||||||
|
|
||||||
|
valid_data = {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'line_item': 1,
|
||||||
|
'quantity': 50,
|
||||||
|
'barcode': 'MY-UNIQUE-BARCODE-123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'line_item': 2,
|
||||||
|
'quantity': 200,
|
||||||
|
'location': 2, # Explicit location
|
||||||
|
'barcode': 'MY-UNIQUE-BARCODE-456',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'location': 1, # Default location
|
||||||
|
}
|
||||||
|
|
||||||
|
# Before posting "valid" data, we will mark the purchase order as "pending"
|
||||||
|
# In this case we do expect an error!
|
||||||
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
|
order.status = PurchaseOrderStatus.PENDING
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
self.url,
|
||||||
|
valid_data,
|
||||||
|
expected_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('can only be received against', str(response.data))
|
||||||
|
|
||||||
|
# Now, set the PO back to "PLACED" so the items can be received
|
||||||
|
order.status = PurchaseOrderStatus.PLACED
|
||||||
|
order.save()
|
||||||
|
|
||||||
# Receive two separate line items against this order
|
# Receive two separate line items against this order
|
||||||
self.post(
|
self.post(
|
||||||
self.url,
|
self.url,
|
||||||
{
|
valid_data,
|
||||||
'items': [
|
|
||||||
{
|
|
||||||
'line_item': 1,
|
|
||||||
'quantity': 50,
|
|
||||||
'barcode': 'MY-UNIQUE-BARCODE-123',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'line_item': 2,
|
|
||||||
'quantity': 200,
|
|
||||||
'location': 2, # Explicit location
|
|
||||||
'barcode': 'MY-UNIQUE-BARCODE-456',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'location': 1, # Default location
|
|
||||||
},
|
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -189,12 +189,15 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
# Process manufacturer part
|
# Process manufacturer part
|
||||||
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
|
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
|
||||||
|
|
||||||
if manufacturer_part:
|
if manufacturer_part and manufacturer_part.manufacturer:
|
||||||
manufacturer_name = manufacturer_part.manufacturer.name
|
manufacturer_name = manufacturer_part.manufacturer.name
|
||||||
else:
|
else:
|
||||||
manufacturer_name = ''
|
manufacturer_name = ''
|
||||||
|
|
||||||
manufacturer_mpn = manufacturer_part.MPN
|
if manufacturer_part:
|
||||||
|
manufacturer_mpn = manufacturer_part.MPN
|
||||||
|
else:
|
||||||
|
manufacturer_mpn = ''
|
||||||
|
|
||||||
# Generate column names for this manufacturer
|
# Generate column names for this manufacturer
|
||||||
k_man = manufacturer_headers[0] + "_" + str(manufacturer_idx)
|
k_man = manufacturer_headers[0] + "_" + str(manufacturer_idx)
|
||||||
@@ -210,12 +213,15 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
# Process supplier parts
|
# Process supplier parts
|
||||||
for supplier_idx, supplier_part in enumerate(manufacturer_part.supplier_parts.all()):
|
for supplier_idx, supplier_part in enumerate(manufacturer_part.supplier_parts.all()):
|
||||||
|
|
||||||
if supplier_part.supplier:
|
if supplier_part.supplier and supplier_part.supplier:
|
||||||
supplier_name = supplier_part.supplier.name
|
supplier_name = supplier_part.supplier.name
|
||||||
else:
|
else:
|
||||||
supplier_name = ''
|
supplier_name = ''
|
||||||
|
|
||||||
supplier_sku = supplier_part.SKU
|
if supplier_part:
|
||||||
|
supplier_sku = supplier_part.SKU
|
||||||
|
else:
|
||||||
|
supplier_sku = ''
|
||||||
|
|
||||||
# Generate column names for this supplier
|
# Generate column names for this supplier
|
||||||
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
|
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class StockItemSerializerBrief(InvenTreeModelSerializer):
|
|||||||
'location',
|
'location',
|
||||||
'location_name',
|
'location_name',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'serial',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-mobile-alt'></span></td>
|
<td><span class='fas fa-mobile-alt'></span></td>
|
||||||
<td>{% trans "Mobile App" %}</td>
|
<td>{% trans "Mobile App" %}</td>
|
||||||
<td><a href="https://inventree.readthedocs.io/en/latest/app/app">https://inventree.readthedocs.io/en/latest/app/app</a></td>
|
<td><a href="{% inventree_docs_url %}/app/app">{% inventree_docs_url %}/app/app</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-bug'></span></td>
|
<td><span class='fas fa-bug'></span></td>
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ function buildFormFields() {
|
|||||||
part_detail: true,
|
part_detail: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sales_order: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
batch: {},
|
batch: {},
|
||||||
target_date: {},
|
target_date: {},
|
||||||
take_from: {},
|
take_from: {},
|
||||||
@@ -76,23 +79,32 @@ function newBuildOrder(options={}) {
|
|||||||
|
|
||||||
var fields = buildFormFields();
|
var fields = buildFormFields();
|
||||||
|
|
||||||
|
// Specify the target part
|
||||||
if (options.part) {
|
if (options.part) {
|
||||||
fields.part.value = options.part;
|
fields.part.value = options.part;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specify the desired quantity
|
||||||
if (options.quantity) {
|
if (options.quantity) {
|
||||||
fields.quantity.value = options.quantity;
|
fields.quantity.value = options.quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specify the parent build order
|
||||||
if (options.parent) {
|
if (options.parent) {
|
||||||
fields.parent.value = options.parent;
|
fields.parent.value = options.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specify a parent sales order
|
||||||
|
if (options.sales_order) {
|
||||||
|
fields.sales_order.value = options.sales_order;
|
||||||
|
}
|
||||||
|
|
||||||
constructForm(`/api/build/`, {
|
constructForm(`/api/build/`, {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
follow: true,
|
follow: true,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: '{% trans "Create Build Order" %}'
|
title: '{% trans "Create Build Order" %}',
|
||||||
|
onSuccess: options.onSuccess,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,8 +635,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
|
|
||||||
var url = '';
|
var url = '';
|
||||||
|
|
||||||
if (row.serial && row.quantity == 1) {
|
|
||||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
var serial = row.serial;
|
||||||
|
|
||||||
|
if (row.stock_item_detail) {
|
||||||
|
serial = row.stock_item_detail.serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serial && row.quantity == 1) {
|
||||||
|
text = `{% trans "Serial Number" %}: ${serial}`;
|
||||||
} else {
|
} else {
|
||||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
loadPurchaseOrderLineItemTable,
|
loadPurchaseOrderLineItemTable,
|
||||||
loadPurchaseOrderTable,
|
loadPurchaseOrderTable,
|
||||||
loadSalesOrderAllocationTable,
|
loadSalesOrderAllocationTable,
|
||||||
|
loadSalesOrderLineItemTable,
|
||||||
loadSalesOrderTable,
|
loadSalesOrderTable,
|
||||||
newPurchaseOrderFromOrderWizard,
|
newPurchaseOrderFromOrderWizard,
|
||||||
newSupplierPartFromOrderWizard,
|
newSupplierPartFromOrderWizard,
|
||||||
@@ -827,3 +828,575 @@ function loadSalesOrderAllocationTable(table, options={}) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an "allocations" sub table, showing stock items allocated againt a sales order
|
||||||
|
* @param {*} index
|
||||||
|
* @param {*} row
|
||||||
|
* @param {*} element
|
||||||
|
*/
|
||||||
|
function showAllocationSubTable(index, row, element, options) {
|
||||||
|
|
||||||
|
// Construct a sub-table element
|
||||||
|
var html = `
|
||||||
|
<div class='sub-table'>
|
||||||
|
<table class='table table-striped table-condensed' id='allocation-table-${row.pk}'>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
element.html(html);
|
||||||
|
|
||||||
|
var table = $(`#allocation-table-${row.pk}`);
|
||||||
|
|
||||||
|
// Is the parent SalesOrder pending?
|
||||||
|
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||||
|
|
||||||
|
// Function to reload the allocation table
|
||||||
|
function reloadTable() {
|
||||||
|
table.bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCallbacks() {
|
||||||
|
// Add callbacks for 'edit' buttons
|
||||||
|
table.find('.button-allocation-edit').click(function() {
|
||||||
|
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// TODO: Migrate to API forms
|
||||||
|
launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, {
|
||||||
|
success: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add callbacks for 'delete' buttons
|
||||||
|
table.find('.button-allocation-delete').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// TODO: Migrate to API forms
|
||||||
|
launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, {
|
||||||
|
success: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
table.bootstrapTable({
|
||||||
|
onPostBody: setupCallbacks,
|
||||||
|
data: row.allocations,
|
||||||
|
showHeader: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'allocated',
|
||||||
|
title: '{% trans "Quantity" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var text = '';
|
||||||
|
|
||||||
|
if (row.serial != null && row.quantity == 1) {
|
||||||
|
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||||
|
} else {
|
||||||
|
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(text, `/stock/item/${row.item}/`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'location',
|
||||||
|
title: '{% trans "Location" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
// Location specified
|
||||||
|
if (row.location) {
|
||||||
|
return renderLink(
|
||||||
|
row.location_detail.pathstring || '{% trans "Location" %}',
|
||||||
|
`/stock/location/${row.location}/`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return `<i>{% trans "Stock location not specified" %}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO: ?? What is 'po' field all about?
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
field: 'po'
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
field: 'buttons',
|
||||||
|
title: '{% trans "Actions" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
var pk = row.pk;
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
|
||||||
|
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a "fulfilled" sub table, showing stock items fulfilled against a purchase order
|
||||||
|
*/
|
||||||
|
function showFulfilledSubTable(index, row, element, options) {
|
||||||
|
// Construct a table showing stock items which have been fulfilled against this line item
|
||||||
|
|
||||||
|
if (!options.order) {
|
||||||
|
return 'ERROR: Order ID not supplied';
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = `fulfilled-table-${row.pk}`;
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class='sub-table'>
|
||||||
|
<table class='table table-striped table-condensed' id='${id}'>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
element.html(html);
|
||||||
|
|
||||||
|
$(`#${id}`).bootstrapTable({
|
||||||
|
url: '{% url "api-stock-list" %}',
|
||||||
|
queryParams: {
|
||||||
|
part: row.part,
|
||||||
|
sales_order: options.order,
|
||||||
|
},
|
||||||
|
showHeader: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stock',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var text = '';
|
||||||
|
if (row.serial && row.quantity == 1) {
|
||||||
|
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||||
|
} else {
|
||||||
|
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(text, `/stock/item/${row.pk}/`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
field: 'po'
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a table displaying line items for a particular SalesOrder
|
||||||
|
*
|
||||||
|
* @param {String} table : HTML ID tag e.g. '#table'
|
||||||
|
* @param {Object} options : object which contains:
|
||||||
|
* - order {integer} : pk of the SalesOrder
|
||||||
|
* - status: {integer} : status code for the order
|
||||||
|
*/
|
||||||
|
function loadSalesOrderLineItemTable(table, options={}) {
|
||||||
|
|
||||||
|
options.params = options.params || {};
|
||||||
|
|
||||||
|
if (!options.order) {
|
||||||
|
console.log('ERROR: function called without order ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.status) {
|
||||||
|
console.log('ERROR: function called without order status');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.params.order = options.order;
|
||||||
|
options.params.part_detail = true;
|
||||||
|
options.params.allocations = true;
|
||||||
|
|
||||||
|
var filters = loadTableFilters('salesorderlineitem');
|
||||||
|
|
||||||
|
for (var key in options.params) {
|
||||||
|
filters[key] = options.params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
options.url = options.url || '{% url "api-so-line-list" %}';
|
||||||
|
|
||||||
|
var filter_target = options.filter_target || '#filter-list-sales-order-lines';
|
||||||
|
|
||||||
|
setupFilterList('salesorderlineitems', $(table), filter_target);
|
||||||
|
|
||||||
|
// Is the order pending?
|
||||||
|
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||||
|
|
||||||
|
// Has the order shipped?
|
||||||
|
var shipped = options.status == {{ SalesOrderStatus.SHIPPED }};
|
||||||
|
|
||||||
|
// Show detail view if the PurchaseOrder is PENDING or SHIPPED
|
||||||
|
var show_detail = pending || shipped;
|
||||||
|
|
||||||
|
// Table columns to display
|
||||||
|
var columns = [
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
checkbox: true,
|
||||||
|
visible: true,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
sortName: 'part__name',
|
||||||
|
field: 'part',
|
||||||
|
title: '{% trans "Part" %}',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
if (row.part) {
|
||||||
|
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
footerFormatter: function() {
|
||||||
|
return '{% trans "Total" %}';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'reference',
|
||||||
|
title: '{% trans "Reference" %}',
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'quantity',
|
||||||
|
title: '{% trans "Quantity" %}',
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
return data.map(function(row) {
|
||||||
|
return +row['quantity'];
|
||||||
|
}).reduce(function(sum, i) {
|
||||||
|
return sum + i;
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'sale_price',
|
||||||
|
title: '{% trans "Unit Price" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return row.sale_price_string || row.sale_price;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
title: '{% trans "Total price" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var total = row.sale_price * row.quantity;
|
||||||
|
var formatter = new Intl.NumberFormat(
|
||||||
|
'en-US',
|
||||||
|
{
|
||||||
|
style: 'currency',
|
||||||
|
currency: row.sale_price_currency
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatter.format(total);
|
||||||
|
},
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
var total = data.map(function(row) {
|
||||||
|
return +row['sale_price'] * row['quantity'];
|
||||||
|
}).reduce(function(sum, i) {
|
||||||
|
return sum + i;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD';
|
||||||
|
|
||||||
|
var formatter = new Intl.NumberFormat(
|
||||||
|
'en-US',
|
||||||
|
{
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatter.format(total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stock',
|
||||||
|
title: '{% trans "In Stock" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return row.part_detail.stock;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'allocated',
|
||||||
|
title: pending ? '{% trans "Allocated" %}' : '{% trans "Fulfilled" %}',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var quantity = pending ? row.allocated : row.fulfilled;
|
||||||
|
return makeProgressBar(quantity, row.quantity, {
|
||||||
|
id: `order-line-progress-${row.pk}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sorter: function(valA, valB, rowA, rowB) {
|
||||||
|
|
||||||
|
var A = pending ? rowA.allocated : rowA.fulfilled;
|
||||||
|
var B = pending ? rowB.allocated : rowB.fulfilled;
|
||||||
|
|
||||||
|
if (A == 0 && B == 0) {
|
||||||
|
return (rowA.quantity > rowB.quantity) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressA = parseFloat(A) / rowA.quantity;
|
||||||
|
var progressB = parseFloat(B) / rowB.quantity;
|
||||||
|
|
||||||
|
return (progressA < progressB) ? 1 : -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'notes',
|
||||||
|
title: '{% trans "Notes" %}',
|
||||||
|
},
|
||||||
|
// TODO: Re-introduce the "PO" field, once it is fixed
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
field: 'po',
|
||||||
|
title: '{% trans "PO" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var po_name = "";
|
||||||
|
if (row.allocated) {
|
||||||
|
row.allocations.forEach(function(allocation) {
|
||||||
|
if (allocation.po != po_name) {
|
||||||
|
if (po_name) {
|
||||||
|
po_name = "-";
|
||||||
|
} else {
|
||||||
|
po_name = allocation.po
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return `<div>` + po_name + `</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
];
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
columns.push({
|
||||||
|
field: 'buttons',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
var pk = row.pk;
|
||||||
|
|
||||||
|
if (row.part) {
|
||||||
|
var part = row.part_detail;
|
||||||
|
|
||||||
|
if (part.trackable) {
|
||||||
|
html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}');
|
||||||
|
|
||||||
|
if (part.purchaseable) {
|
||||||
|
html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.assembly) {
|
||||||
|
html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
|
||||||
|
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}');
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove the "in stock" column
|
||||||
|
delete columns['stock'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadTable() {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure callback functions once the table is loaded
|
||||||
|
function setupCallbacks() {
|
||||||
|
|
||||||
|
// Callback for editing line items
|
||||||
|
$(table).find('.button-edit').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
constructForm(`/api/order/so-line/${pk}/`, {
|
||||||
|
fields: {
|
||||||
|
quantity: {},
|
||||||
|
reference: {},
|
||||||
|
sale_price: {},
|
||||||
|
sale_price_currency: {},
|
||||||
|
notes: {},
|
||||||
|
},
|
||||||
|
title: '{% trans "Edit Line Item" %}',
|
||||||
|
onSuccess: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for deleting line items
|
||||||
|
$(table).find('.button-delete').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
constructForm(`/api/order/so-line/${pk}/`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
title: '{% trans "Delete Line Item" %}',
|
||||||
|
onSuccess: reloadTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for allocating stock items by serial number
|
||||||
|
$(table).find('.button-add-by-sn').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// TODO: Migrate this form to the API forms
|
||||||
|
inventreeGet(`/api/order/so-line/${pk}/`, {},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
launchModalForm('{% url "so-assign-serials" %}', {
|
||||||
|
success: reloadTable,
|
||||||
|
data: {
|
||||||
|
line: pk,
|
||||||
|
part: response.part,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for allocation stock items to the order
|
||||||
|
$(table).find('.button-add').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// TODO: Migrate this form to the API forms
|
||||||
|
launchModalForm(`/order/sales-order/allocation/new/`, {
|
||||||
|
success: reloadTable,
|
||||||
|
data: {
|
||||||
|
line: pk,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for creating a new build
|
||||||
|
$(table).find('.button-build').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
// Extract the row data from the table!
|
||||||
|
var idx = $(this).closest('tr').attr('data-index');
|
||||||
|
|
||||||
|
var row = $(table).bootstrapTable('getData')[idx];
|
||||||
|
|
||||||
|
var quantity = 1;
|
||||||
|
|
||||||
|
if (row.allocated < row.quantity) {
|
||||||
|
quantity = row.quantity - row.allocated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new build order
|
||||||
|
newBuildOrder({
|
||||||
|
part: pk,
|
||||||
|
sales_order: options.order,
|
||||||
|
quantity: quantity,
|
||||||
|
success: reloadTable
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for purchasing parts
|
||||||
|
$(table).find('.button-buy').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm('{% url "order-parts" %}', {
|
||||||
|
data: {
|
||||||
|
parts: [
|
||||||
|
pk
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for displaying price
|
||||||
|
$(table).find('.button-price').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
var idx = $(this).closest('tr').attr('data-index');
|
||||||
|
var row = $(table).bootstrapTable('getData')[idx];
|
||||||
|
|
||||||
|
launchModalForm(
|
||||||
|
'{% url "line-pricing" %}',
|
||||||
|
{
|
||||||
|
submit_text: '{% trans "Calculate price" %}',
|
||||||
|
data: {
|
||||||
|
line_item: pk,
|
||||||
|
quantity: row.quantity,
|
||||||
|
},
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: 'update_price',
|
||||||
|
title: '{% trans "Update Unit Price" %}'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
success: reloadTable,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(table).inventreeTable({
|
||||||
|
onPostBody: setupCallbacks,
|
||||||
|
name: 'salesorderlineitems',
|
||||||
|
sidePagination: 'server',
|
||||||
|
formatNoMatches: function() {
|
||||||
|
return '{% trans "No matching line items" %}';
|
||||||
|
},
|
||||||
|
queryParams: filters,
|
||||||
|
original: options.params,
|
||||||
|
url: options.url,
|
||||||
|
showFooter: true,
|
||||||
|
uniqueId: 'pk',
|
||||||
|
detailView: show_detail,
|
||||||
|
detailViewByClick: show_detail,
|
||||||
|
detailFilter: function(index, row) {
|
||||||
|
if (pending) {
|
||||||
|
// Order is pending
|
||||||
|
return row.allocated > 0;
|
||||||
|
} else {
|
||||||
|
return row.fulfilled > 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detailFormatter: function(index, row, element) {
|
||||||
|
if (pending) {
|
||||||
|
return showAllocationSubTable(index, row, element, options);
|
||||||
|
} else {
|
||||||
|
return showFulfilledSubTable(index, row, element, options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns: columns,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<td><span class='fas fa-tasks'></span></td>
|
<td><span class='fas fa-tasks'></span></td>
|
||||||
<td>{% trans "Background Worker" %}</td>
|
<td>{% trans "Background Worker" %}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href='https://inventree.readthedocs.io/en/latest/admin/tasks'>
|
<a href='{% inventree_docs_url %}/admin/tasks'>
|
||||||
<span class='label label-red'>{% trans "Background worker not running" %}</span>
|
<span class='label label-red'>{% trans "Background worker not running" %}</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<td><span class='fas fa-envelope'></span></td>
|
<td><span class='fas fa-envelope'></span></td>
|
||||||
<td>{% trans "Email Settings" %}</td>
|
<td>{% trans "Email Settings" %}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href='https://inventree.readthedocs.io/en/latest/admin/email'>
|
<a href='{% inventree_docs_url %}/admin/email'>
|
||||||
<span class='label label-yellow'>{% trans "Email settings not configured" %}</span>
|
<span class='label label-yellow'>{% trans "Email settings not configured" %}</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import sys
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
import requests
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
@@ -16,9 +17,14 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
||||||
|
|
||||||
|
version = None
|
||||||
|
|
||||||
with open(version_file, 'r') as f:
|
with open(version_file, 'r') as f:
|
||||||
|
|
||||||
results = re.findall(r'INVENTREE_SW_VERSION = "(.*)"', f.read())
|
text = f.read()
|
||||||
|
|
||||||
|
# Extract the InvenTree software version
|
||||||
|
results = re.findall(r'INVENTREE_SW_VERSION = "(.*)"', text)
|
||||||
|
|
||||||
if not len(results) == 1:
|
if not len(results) == 1:
|
||||||
print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
|
print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
|
||||||
@@ -26,6 +32,8 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
version = results[0]
|
version = results[0]
|
||||||
|
|
||||||
|
print(f"InvenTree Version: '{version}'")
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-t', '--tag', help='Compare against specified version tag', action='store')
|
parser.add_argument('-t', '--tag', help='Compare against specified version tag', action='store')
|
||||||
parser.add_argument('-r', '--release', help='Check that this is a release version', action='store_true')
|
parser.add_argument('-r', '--release', help='Check that this is a release version', action='store_true')
|
||||||
@@ -57,6 +65,8 @@ if __name__ == '__main__':
|
|||||||
e.g. "0.5 dev"
|
e.g. "0.5 dev"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
print(f"Checking development branch")
|
||||||
|
|
||||||
pattern = "^\d+(\.\d+)+ dev$"
|
pattern = "^\d+(\.\d+)+ dev$"
|
||||||
|
|
||||||
result = re.match(pattern, version)
|
result = re.match(pattern, version)
|
||||||
@@ -71,6 +81,8 @@ if __name__ == '__main__':
|
|||||||
e.g. "0.5.1"
|
e.g. "0.5.1"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
print(f"Checking release branch")
|
||||||
|
|
||||||
pattern = "^\d+(\.\d+)+$"
|
pattern = "^\d+(\.\d+)+$"
|
||||||
|
|
||||||
result = re.match(pattern, version)
|
result = re.match(pattern, version)
|
||||||
@@ -84,4 +96,4 @@ if __name__ == '__main__':
|
|||||||
print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'")
|
print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user