2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 04:26:44 +00:00

Merge pull request #2091 from inventree/0.5.1

0.5.1
This commit is contained in:
Oliver 2021-10-11 21:20:15 +11:00 committed by GitHub
commit f948290b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 29241 additions and 25821 deletions

View File

@ -274,7 +274,9 @@ def send_email(subject, body, recipients, from_email=None):
offload_task(
'django.core.mail.send_mail',
subject, body,
subject,
body,
from_email,
recipients,
fail_silently=False,
)

View File

@ -8,8 +8,9 @@ import re
import common.models
INVENTREE_SW_VERSION = "0.5.0"
INVENTREE_SW_VERSION = "0.5.1"
# InvenTree API version
INVENTREE_API_VERSION = 12
"""
@ -96,16 +97,14 @@ def inventreeDocsVersion():
Return the version string matching the latest documentation.
Development -> "latest"
Release -> "major.minor"
Release -> "major.minor.sub" e.g. "0.5.2"
"""
if isInvenTreeDevelopmentVersion():
return "latest"
else:
major, minor, patch = inventreeVersionTuple()
return f"{major}.{minor}"
return INVENTREE_SW_VERSION
def isInvenTreeUpToDate():

View File

@ -292,6 +292,7 @@ loadStockTable($("#build-stock-table"), {
location_detail: true,
part_detail: true,
build: {{ build.id }},
is_building: false,
},
groupByField: 'location',
buttons: [

View File

@ -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'},
),
]

View File

@ -94,6 +94,7 @@ class Company(models.Model):
constraints = [
UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair')
]
verbose_name_plural = "Companies"
name = models.CharField(max_length=100, blank=False,
help_text=_('Company name'),

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

@ -8,11 +8,13 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.conf.urls import url, include
from django.db import transaction
from django.core.exceptions import ValidationError as DjangoValidationError
from django_filters import rest_framework as rest_filters
from rest_framework import generics
from rest_framework import filters, status
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.serializers import ValidationError
@ -243,10 +245,11 @@ class POReceive(generics.CreateAPIView):
pk = self.kwargs.get('pk', None)
if pk is None:
return None
else:
order = PurchaseOrder.objects.get(pk=self.kwargs['pk'])
try:
order = PurchaseOrder.objects.get(pk=pk)
except (PurchaseOrder.DoesNotExist, ValueError):
raise ValidationError(_("Matching purchase order does not exist"))
return order
def create(self, request, *args, **kwargs):
@ -259,9 +262,14 @@ class POReceive(generics.CreateAPIView):
serializer.is_valid(raise_exception=True)
# Receive the line items
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)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@transaction.atomic

View File

@ -418,16 +418,24 @@ class PurchaseOrder(Order):
barcode = ''
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:
if not (quantity % 1 == 0):
raise ValidationError({"quantity": _("Quantity must be an integer")})
raise ValidationError({
"quantity": _("Quantity must be an integer")
})
if quantity < 0:
raise ValidationError({"quantity": _("Quantity must be a positive number")})
raise ValidationError({
"quantity": _("Quantity must be a positive number")
})
quantity = int(quantity)
except (ValueError, TypeError):
raise ValidationError({"quantity": _("Invalid quantity provided")})
raise ValidationError({
"quantity": _("Invalid quantity provided")
})
# Create a new stock item
if line.part and quantity > 0:

View File

@ -235,6 +235,7 @@ class POLineItemReceiveSerializer(serializers.Serializer):
help_text=_('Unique identifier field'),
default='',
required=False,
allow_blank=True,
)
def validate_barcode(self, barcode):
@ -494,7 +495,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
order_detail = SalesOrderSerializer(source='order', 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()

View File

@ -179,446 +179,17 @@ $("#new-so-line").click(function() {
});
});
{% 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: [
loadSalesOrderLineItemTable(
'#so-lines-table',
{
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: {
quantity: {},
reference: {},
sale_price: {},
sale_price_currency: {},
notes: {},
},
title: '{% trans "Edit Line Item" %}',
onSuccess: reloadTable,
});
});
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,
});
});
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,
}
});
}
order: {{ order.pk }},
status: {{ order.status }},
}
);
});
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({
name: 'sales-order',
default: 'order-items'
});
}
{% endblock %}

View File

@ -401,10 +401,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(line_1.received, 0)
self.assertEqual(line_2.received, 50)
# Receive two separate line items against this order
self.post(
self.url,
{
valid_data = {
'items': [
{
'line_item': 1,
@ -419,7 +416,30 @@ class PurchaseOrderReceiveTest(OrderTest):
}
],
'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
self.post(
self.url,
valid_data,
expected_code=201,
)

View File

@ -189,12 +189,15 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Process manufacturer part
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
else:
manufacturer_name = ''
if manufacturer_part:
manufacturer_mpn = manufacturer_part.MPN
else:
manufacturer_mpn = ''
# Generate column names for this manufacturer
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
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
else:
supplier_name = ''
if supplier_part:
supplier_sku = supplier_part.SKU
else:
supplier_sku = ''
# Generate column names for this supplier
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)

View File

@ -64,6 +64,7 @@ class StockItemSerializerBrief(InvenTreeModelSerializer):
'location',
'location_name',
'quantity',
'serial',
]

View File

@ -83,7 +83,7 @@
<tr>
<td><span class='fas fa-mobile-alt'></span></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>
<td><span class='fas fa-bug'></span></td>

View File

@ -42,6 +42,9 @@ function buildFormFields() {
part_detail: true,
}
},
sales_order: {
hidden: true,
},
batch: {},
target_date: {},
take_from: {},
@ -76,23 +79,32 @@ function newBuildOrder(options={}) {
var fields = buildFormFields();
// Specify the target part
if (options.part) {
fields.part.value = options.part;
}
// Specify the desired quantity
if (options.quantity) {
fields.quantity.value = options.quantity;
}
// Specify the parent build order
if (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/`, {
fields: fields,
follow: true,
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 = '';
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 {
text = `{% trans "Quantity" %}: ${row.quantity}`;
}

View File

@ -23,6 +23,7 @@
loadPurchaseOrderLineItemTable,
loadPurchaseOrderTable,
loadSalesOrderAllocationTable,
loadSalesOrderLineItemTable,
loadSalesOrderTable,
newPurchaseOrderFromOrderWizard,
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,
});
}

View File

@ -51,7 +51,7 @@
<td><span class='fas fa-tasks'></span></td>
<td>{% trans "Background Worker" %}</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>
</a>
</td>
@ -62,7 +62,7 @@
<td><span class='fas fa-envelope'></span></td>
<td>{% trans "Email Settings" %}</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>
</a>
</td>

View File

@ -9,6 +9,7 @@ import sys
import re
import os
import argparse
import requests
if __name__ == '__main__':
@ -16,9 +17,14 @@ if __name__ == '__main__':
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
version = None
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:
print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
@ -26,6 +32,8 @@ if __name__ == '__main__':
version = results[0]
print(f"InvenTree Version: '{version}'")
parser = argparse.ArgumentParser()
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')
@ -57,6 +65,8 @@ if __name__ == '__main__':
e.g. "0.5 dev"
"""
print(f"Checking development branch")
pattern = "^\d+(\.\d+)+ dev$"
result = re.match(pattern, version)
@ -71,6 +81,8 @@ if __name__ == '__main__':
e.g. "0.5.1"
"""
print(f"Checking release branch")
pattern = "^\d+(\.\d+)+$"
result = re.match(pattern, version)