Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2021-01-31 20:46:09 +11:00
commit 4b728520ac
25 changed files with 1843 additions and 1428 deletions

View File

@ -196,23 +196,23 @@ INSTALLED_APPS = [
'users.apps.UsersConfig',
# Third part add-ons
'django_filters', # Extended filter functionality
'dbbackup', # Database backup / restore
'rest_framework', # DRF (Django Rest Framework)
'rest_framework.authtoken', # Token authentication for API
'corsheaders', # Cross-origin Resource Sharing for DRF
'crispy_forms', # Improved form rendering
'import_export', # Import / export tables to file
'django_cleanup', # Automatically delete orphaned MEDIA files
'qr_code', # Generate QR codes
'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing
'markdownify', # Markdown template rendering
'django_tex', # LaTeX output
'django_admin_shell', # Python shell for the admin interface
'djmoney', # django-money integration
'djmoney.contrib.exchange', # django-money exchange rates
'error_report', # Error reporting in the admin interface
'django_filters', # Extended filter functionality
'dbbackup', # Database backup / restore
'rest_framework', # DRF (Django Rest Framework)
'rest_framework.authtoken', # Token authentication for API
'corsheaders', # Cross-origin Resource Sharing for DRF
'crispy_forms', # Improved form rendering
'import_export', # Import / export tables to file
'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files
'qr_code', # Generate QR codes
'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing
'markdownify', # Markdown template rendering
'django_tex', # LaTeX output
'django_admin_shell', # Python shell for the admin interface
'djmoney', # django-money integration
'djmoney.contrib.exchange', # django-money exchange rates
'error_report', # Error reporting in the admin interface
]
MIDDLEWARE = CONFIG.get('middleware', [

View File

@ -8,11 +8,33 @@ function deleteButton(url, text='Delete') {
}
function renderLink(text, url) {
if (text === '' || url === '') {
function renderLink(text, url, options={}) {
if (url == null || url === '') {
return text;
}
var max_length = options.max_length || -1;
var remove_http = options.remove_http || false;
if (remove_http) {
if (text.startsWith('http://')) {
text = text.slice(7);
} else if (text.startsWith('https://')) {
text = text.slice(8);
}
}
// Shorten the displayed length if required
if ((max_length > 0) && (text.length > max_length)) {
var slice_length = (max_length - 3) / 2;
var text_start = text.slice(0, slice_length);
var text_end = text.slice(-slice_length);
text = `${text_start}...${text_end}`;
}
return '<a href="' + url + '">' + text + '</a>';
}

View File

@ -90,7 +90,7 @@ class BarcodeScan(APIView):
if loc is not None:
response['stocklocation'] = plugin.renderStockLocation(loc)
response['url'] = reverse('location-detail', kwargs={'pk': loc.id})
response['url'] = reverse('stock-location-detail', kwargs={'pk': loc.id})
match_found = True
# Try to associate with a part

View File

@ -225,22 +225,19 @@ class Build(MPTTModel):
blank=True, help_text=_('Extra build notes')
)
@property
def is_overdue(self):
"""
Returns true if this build is "overdue":
- Not completed
- Target date is "in the past"
Makes use of the OVERDUE_FILTER to avoid code duplication
"""
# Cannot be deemed overdue if target_date is not set
if self.target_date is None:
return False
today = datetime.now().date()
return self.active and self.target_date < today
query = Build.objects.filter(pk=self.pk)
query = query.filter(Build.OVERDUE_FILTER)
return query.exists()
@property
def active(self):
"""

View File

@ -10,6 +10,7 @@ from rest_framework.test import APITestCase
from rest_framework import status
import json
from datetime import datetime, timedelta
from .models import Build
from stock.models import StockItem
@ -70,6 +71,24 @@ class BuildTestSimple(TestCase):
self.assertEqual(b2.status, BuildStatus.COMPLETE)
def test_overdue(self):
"""
Test overdue status functionality
"""
today = datetime.now().date()
build = Build.objects.get(pk=1)
self.assertFalse(build.is_overdue)
build.target_date = today - timedelta(days=1)
build.save()
self.assertTrue(build.is_overdue)
build.target_date = today + timedelta(days=80)
build.save()
self.assertFalse(build.is_overdue)
def test_is_active(self):
b1 = Build.objects.get(pk=1)
b2 = Build.objects.get(pk=2)

View File

@ -71,6 +71,13 @@ class InvenTreeSetting(models.Model):
'choices': djmoney.settings.CURRENCY_CHOICES,
},
'BARCODE_ENABLE': {
'name': _('Barcode Support'),
'description': _('Enable barcode scanner support'),
'default': True,
'validator': bool,
},
'PART_IPN_REGEX': {
'name': _('IPN Regex'),
'description': _('Regular expression pattern for matching Part IPN')

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

@ -272,6 +272,7 @@ class PurchaseOrder(Order):
self.complete_date = datetime.now().date()
self.save()
@property
def is_overdue(self):
"""
Returns True if this PurchaseOrder is "overdue"
@ -455,7 +456,7 @@ class SalesOrder(Order):
"""
query = SalesOrder.objects.filter(pk=self.pk)
query = query.filer(SalesOrder.OVERDUE_FILTER)
query = query.filter(SalesOrder.OVERDUE_FILTER)
return query.exists()

View File

@ -5,6 +5,8 @@ from django.test import TestCase
from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from datetime import datetime, timedelta
from company.models import Company
from stock.models import StockItem
from order.models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation
@ -40,6 +42,26 @@ class SalesOrderTest(TestCase):
# Create a line item
self.line = SalesOrderLineItem.objects.create(quantity=50, order=self.order, part=self.part)
def test_overdue(self):
"""
Tests for overdue functionality
"""
today = datetime.now().date()
# By default, order is *not* overdue as the target date is not set
self.assertFalse(self.order.is_overdue)
# Set target date in the past
self.order.target_date = today - timedelta(days=5)
self.order.save()
self.assertTrue(self.order.is_overdue)
# Set target date in the future
self.order.target_date = today + timedelta(days=5)
self.order.save()
self.assertFalse(self.order.is_overdue)
def test_empty_order(self):
self.assertEqual(self.line.quantity, 50)
self.assertEqual(self.line.allocated_quantity(), 0)

View File

@ -1,3 +1,7 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from django.test import TestCase
import django.core.exceptions as django_exceptions
@ -37,6 +41,24 @@ class OrderTest(TestCase):
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)")
def test_overdue(self):
"""
Test overdue status functionality
"""
today = datetime.now().date()
order = PurchaseOrder.objects.get(pk=1)
self.assertFalse(order.is_overdue)
order.target_date = today - timedelta(days=5)
order.save()
self.assertTrue(order.is_overdue)
order.target_date = today + timedelta(days=1)
order.save()
self.assertFalse(order.is_overdue)
def test_increment(self):
next_ref = PurchaseOrder.getNextOrderNumber()

View File

@ -6,6 +6,7 @@ Part database model definitions
from __future__ import unicode_literals
import os
import logging
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
@ -51,6 +52,9 @@ import common.models
import part.settings as part_settings
logger = logging.getLogger(__name__)
class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects.
@ -335,11 +339,14 @@ class Part(MPTTModel):
if self.pk:
previous = Part.objects.get(pk=self.pk)
if previous.image and not self.image == previous.image:
# Image has been changed
if previous.image is not None and not self.image == previous.image:
# Are there any (other) parts which reference the image?
n_refs = Part.objects.filter(image=previous.image).exclude(pk=self.pk).count()
if n_refs == 0:
logger.info(f"Deleting unused image file '{previous.image}'")
previous.image.delete(save=False)
self.clean()
@ -710,7 +717,7 @@ class Part(MPTTModel):
null=True,
blank=True,
variations={'thumbnail': (128, 128)},
delete_orphans=True,
delete_orphans=False,
)
default_location = TreeForeignKey(

View File

@ -44,6 +44,8 @@
<span id='part-star-icon' class='fas fa-star {% if starred %}icon-yellow{% endif %}'/>
</button>
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group'>
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
@ -52,6 +54,7 @@
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
</ul>
</div>
{% endif %}
{% if part.active %}
<button type='button' class='btn btn-default' id='price-button' title='{% trans "Show pricing information" %}'>
<span id='part-price-icon' class='fas fa-dollar-sign'/>

View File

@ -121,12 +121,17 @@ class StockAdjust(APIView):
- StockAdd: add stock items
- StockRemove: remove stock items
- StockTransfer: transfer stock items
# TODO - This needs serious refactoring!!!
"""
permission_classes = [
permissions.IsAuthenticated,
]
allow_missing_quantity = False
def get_items(self, request):
"""
Return a list of items posted to the endpoint.
@ -157,10 +162,13 @@ class StockAdjust(APIView):
except (ValueError, StockItem.DoesNotExist):
raise ValidationError({'pk': 'Each entry must contain a valid pk field'})
if self.allow_missing_quantity and 'quantity' not in entry:
entry['quantity'] = item.quantity
try:
quantity = Decimal(str(entry.get('quantity', None)))
except (ValueError, TypeError, InvalidOperation):
raise ValidationError({'quantity': 'Each entry must contain a valid quantity field'})
raise ValidationError({'quantity': "Each entry must contain a valid quantity value"})
if quantity < 0:
raise ValidationError({'quantity': 'Quantity field must not be less than zero'})
@ -234,6 +242,8 @@ class StockTransfer(StockAdjust):
API endpoint for performing stock movements
"""
allow_missing_quantity = True
def post(self, request, *args, **kwargs):
self.get_items(request)

View File

@ -120,6 +120,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
</div>
<div class='btn-group action-buttons' role='group'>
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group'>
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
@ -127,24 +129,26 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
{% if roles.stock.change %}
{% if item.uid %}
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
{% else %}
<li><a href='#' id='link-barcode'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>
{% endif %}
{% endif %}
</ul>
</div>
<!-- Document / label menu -->
{% if item.has_labels or item.has_test_reports %}
<div class='btn-group'>
<button id='document-options' title='{% trans "Printing actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-print'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.has_labels %}
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
{% endif %}
{% if item.has_test_reports %}
<li><a href='#' id='stock-test-report'><span class='fas fa-file-pdf'></span> {% trans "Test Report" %}</a></li>
{% endif %}
<li><a href='#' id='barcode-unlink'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
{% else %}
<li><a href='#' id='barcode-link'><span class='fas fa-link'></span> {% trans "Link Barcode" %}</a></li>
{% endif %}
<li><a href='#' id='barcode-scan-into-location'><span class='fas fa-sitemap'></span> {% trans "Scan to Location" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
<!-- Document / label menu -->
{% if item.has_labels or item.has_test_reports %}
<div class='btn-group'>
<button id='document-options' title='{% trans "Printing actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-print'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.has_labels %}
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
{% endif %}
{% if item.has_test_reports %}
<li><a href='#' id='stock-test-report'><span class='fas fa-file-pdf'></span> {% trans "Test Report" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
@ -447,14 +451,18 @@ $("#show-qr-code").click(function() {
});
});
$("#link-barcode").click(function() {
$("#barcode-link").click(function() {
linkBarcodeDialog({{ item.id }});
});
$("#unlink-barcode").click(function() {
$("#barcode-unlink").click(function() {
unlinkBarcode({{ item.id }});
});
$("#barcode-scan-into-location").click(function() {
scanItemsIntoLocation([{{ item.id }}]);
});
{% if item.in_stock %}
$("#stock-assign-to-customer").click(function() {

View File

@ -37,6 +37,8 @@
</button>
{% endif %}
{% endif %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% if barcodes %}
<!-- Barcode actions menu -->
{% if location %}
<div class='btn-group'>
@ -47,29 +49,30 @@
<li><a href='#' id='barcode-check-in'><span class='fas fa-arrow-right'></span> {% trans "Check-in Items" %}</a></li>
</ul>
</div>
<!-- Check permissions and owner -->
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
{% if roles.stock.change %}
<div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock 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'>
<li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span>
{% trans "Count stock" %}</a></li>
</ul>
</div>
{% endif %}
{% if roles.stock_location.change %}
<div class='btn-group'>
<button id='location-actions' title='{% trans "Location actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle="dropdown"><span class='fas fa-sitemap'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
<li><a href='#' id='location-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit location" %}</a></li>
{% if roles.stock.delete %}
<li><a href='#' id='location-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete location" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
<!-- Check permissions and owner -->
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
{% if roles.stock.change %}
<div class='btn-group'>
<button id='stock-actions' title='{% trans "Stock 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'>
<li><a href='#' id='location-count'><span class='fas fa-clipboard-list'></span>
{% trans "Count stock" %}</a></li>
</ul>
</div>
{% endif %}
{% if roles.stock_location.change %}
<div class='btn-group'>
<button id='location-actions' title='{% trans "Location actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle="dropdown"><span class='fas fa-sitemap'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
<li><a href='#' id='location-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit location" %}</a></li>
{% if roles.stock.delete %}
<li><a href='#' id='location-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete location" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>

View File

@ -21,4 +21,12 @@
</tbody>
</table>
<h4>{% trans "Barcode Settings" %}</h4>
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %}
</tbody>
</table>
{% endblock %}

View File

@ -2,6 +2,8 @@
{% load i18n %}
{% load inventree_extras %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
<!DOCTYPE html>
<html lang="en">
<head>
@ -145,9 +147,11 @@ $(document).ready(function () {
showCachedAlerts();
{% if barcodes %}
$('#barcode-scan').click(function() {
barcodeScanDialog();
});
{% endif %}
});

View File

@ -1,12 +1,14 @@
{% load i18n %}
function makeBarcodeInput(placeholderText='') {
function makeBarcodeInput(placeholderText='', hintText='') {
/*
* Generate HTML for a barcode input
*/
placeholderText = placeholderText || '{% trans "Scan barcode data here using wedge scanner" %}';
hintText = hintText || '{% trans "Enter barcode data" %}';
var html = `
<div class='form-group'>
<label class='control-label' for='barcode'>{% trans "Barcode" %}</label>
@ -17,7 +19,7 @@ function makeBarcodeInput(placeholderText='') {
</span>
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
</div>
<div id='hint_barcode_data' class='help-block'>{% trans "Enter barcode data" %}</div>
<div id='hint_barcode_data' class='help-block'>${hintText}</div>
</div>
</div>
`;
@ -25,6 +27,81 @@ function makeBarcodeInput(placeholderText='') {
return html;
}
function makeNotesField(options={}) {
var tooltip = options.tooltip || '{% trans "Enter optional notes for stock transfer" %}';
var placeholder = options.placeholder || '{% trans "Enter notes" %}';
return `
<div class='form-group'>
<label class='control-label' for='notes'>{% trans "Notes" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='fas fa-sticky-note'></span>
</span>
<input id='notes' class='textinput textInput form-control' type='text' name='notes' placeholder='${placeholder}'>
</div>
<div id='hint_notes' class='help_block'>${tooltip}</div>
</div>
</div>`;
}
/*
* POST data to the server, and handle standard responses.
*/
function postBarcodeData(barcode_data, options={}) {
var modal = options.modal || '#modal-form';
var url = options.url || '/api/barcode/';
var data = options.data || {};
data.barcode = barcode_data;
inventreePut(
url,
data,
{
method: 'POST',
error: function() {
enableBarcodeInput(modal, true);
showBarcodeMessage(modal, '{% trans "Server error" %}');
},
success: function(response, status) {
modalEnable(modal, false);
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('success' in response) {
if (options.onScan) {
options.onScan(response);
}
} else if ('error' in response) {
showBarcodeMessage(
modal,
response.error,
'warning'
);
} else {
showBarcodeMessage(
modal,
'{% trans "Unknown response from server" %}',
'warning'
);
}
} else {
// Invalid response returned from server
showInvalidResponseError(modal, response, status);
}
}
}
)
}
function showBarcodeMessage(modal, message, style='danger') {
@ -43,12 +120,6 @@ function showInvalidResponseError(modal, response, status) {
}
function clearBarcodeError(modal, message) {
$(modal + ' #barcode-error-message').html('');
}
function enableBarcodeInput(modal, enabled=true) {
var barcode = $(modal + ' #barcode');
@ -87,9 +158,7 @@ function barcodeDialog(title, options={}) {
if (barcode && barcode.length > 0) {
if (options.onScan) {
options.onScan(barcode);
}
postBarcodeData(barcode, options);
}
}
@ -189,40 +258,20 @@ function barcodeScanDialog() {
barcodeDialog(
"Scan Barcode",
{
onScan: function(barcode) {
enableBarcodeInput(modal, false);
inventreePut(
'/api/barcode/',
{
barcode: barcode,
},
{
method: 'POST',
success: function(response, status) {
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('success' in response) {
if ('url' in response) {
// Redirect to the URL!
$(modal).modal('hide');
window.location.href = response.url;
}
} else if ('error' in response) {
showBarcodeMessage(modal, response.error, 'warning');
} else {
showBarcodeMessage(modal, "{% trans 'Unknown response from server' %}", 'warning');
}
} else {
showInvalidResponseError(modal, response, status);
}
},
},
);
},
onScan: function(response) {
if ('url' in response) {
$(modal).modal('hide');
// Redirect to the URL!
window.location.href = response.url;
} else {
showBarcodeMessage(
modal,
'{% trans "No URL in response" %}',
'warning'
);
}
}
},
);
}
@ -238,37 +287,14 @@ function linkBarcodeDialog(stockitem, options={}) {
barcodeDialog(
"{% trans 'Link Barcode to Stock Item' %}",
{
onScan: function(barcode) {
enableBarcodeInput(modal, false);
inventreePut(
'/api/barcode/link/',
{
barcode: barcode,
stockitem: stockitem,
},
{
method: 'POST',
success: function(response, status) {
url: '/api/barcode/link/',
data: {
stockitem: stockitem,
},
onScan: function(response) {
enableBarcodeInput(modal, true);
if (status == 'success') {
if ('success' in response) {
$(modal).modal('hide');
location.reload();
} else if ('error' in response) {
showBarcodeMessage(modal, response.error, 'warning');
} else {
showBarcodeMessage(modal, "{% trans 'Unknown response from server' %}", warning);
}
} else {
showInvalidResponseError(modal, response, status);
}
},
},
);
$(modal).modal('hide');
location.reload();
}
}
);
@ -386,22 +412,10 @@ function barcodeCheckIn(location_id, options={}) {
var table = `<div class='container' id='items-table-div' style='width: 80%; float: left;'></div>`;
// Extra form fields
var extra = `
<div class='form-group'>
<label class='control-label' for='notes'>{% trans "Notes" %}</label>
<div class='controls'>
<div class='input-group'>
<span class='input-group-addon'>
<span class='fas fa-sticky-note'></span>
</span>
<input id='notes' class='textinput textInput form-control' type='text' name='notes' placeholder='{% trans "Enter notes" %}'>
</div>
<div id='hint_notes' class='help_block'>{% trans "Enter optional notes for stock transfer" %}</div>
</div>
</div>`;
var extra = makeNotesField();
barcodeDialog(
"{% trans "Check Stock Items into Location" %}",
'{% trans "Check Stock Items into Location" %}',
{
headerContent: table,
preShow: function() {
@ -414,7 +428,6 @@ function barcodeCheckIn(location_id, options={}) {
extraFields: extra,
onSubmit: function() {
// Called when the 'check-in' button is pressed
var data = {location: location_id};
@ -434,7 +447,7 @@ function barcodeCheckIn(location_id, options={}) {
data.items = entries;
inventreePut(
'{% url 'api-stock-transfer' %}',
"{% url 'api-stock-transfer' %}",
data,
{
method: 'POST',
@ -446,69 +459,154 @@ function barcodeCheckIn(location_id, options={}) {
showAlertOrCache('alert-success', response.success, true);
location.reload();
} else {
showAlertOrCache('alert-success', 'Error transferring stock', false);
showAlertOrCache('alert-success', '{% trans "Error transferring stock" %}', false);
}
}
}
);
},
onScan: function(barcode) {
enableBarcodeInput(modal, false);
inventreePut(
'/api/barcode/',
{
barcode: barcode,
},
{
method: 'POST',
error: function() {
enableBarcodeInput(modal, true);
showBarcodeMessage(modal, '{% trans "Server error" %}');
},
success: function(response, status) {
onScan: function(response) {
if ('stockitem' in response) {
stockitem = response.stockitem;
enableBarcodeInput(modal, true);
var duplicate = false;
if (status == 'success') {
if ('stockitem' in response) {
stockitem = response.stockitem;
items.forEach(function(item) {
if (item.pk == stockitem.pk) {
duplicate = true;
}
});
var duplicate = false;
if (duplicate) {
showBarcodeMessage(modal, '{% trans "Stock Item already scanned" %}', "warning");
} else {
items.forEach(function(item) {
if (item.pk == stockitem.pk) {
duplicate = true;
}
});
if (stockitem.location == location_id) {
showBarcodeMessage(modal, '{% trans "Stock Item already in this location" %}');
return;
}
if (duplicate) {
showBarcodeMessage(modal, "{% trans "Stock Item already scanned" %}", "warning");
} else {
// Add this stock item to the list
items.push(stockitem);
if (stockitem.location == location_id) {
showBarcodeMessage(modal, "{% trans "Stock Item already in this location" %}");
return;
}
showBarcodeMessage(modal, '{% trans "Added stock item" %}', "success");
// Add this stock item to the list
items.push(stockitem);
reloadTable();
}
showBarcodeMessage(modal, "{% trans "Added stock item" %}", "success");
reloadTable();
}
} else {
// Barcode does not match a stock item
showBarcodeMessage(modal, "{% trans "Barcode does not match Stock Item" %}", "warning");
}
} else {
showInvalidResponseError(modal, response, status);
}
},
},
);
} else {
// Barcode does not match a stock item
showBarcodeMessage(modal, '{% trans "Barcode does not match Stock Item" %}', "warning");
}
},
}
);
}
/*
* Display dialog to check a single stock item into a stock location
*/
function scanItemsIntoLocation(item_id_list, options={}) {
var modal = options.modal || '#modal-form';
var stock_location = null;
// Extra form fields
var extra = makeNotesField();
// Header contentfor
var header = `
<div id='header-div'>
</div>
`;
function updateLocationInfo(location) {
var div = $(modal + ' #header-div');
if (stock_location && stock_location.pk) {
div.html(`
<div class='alert alert-block alert-info'>
<b>{% trans "Location" %}</b></br>
${stock_location.name}<br>
<i>${stock_location.description}</i>
</div>
`);
} else {
div.html('');
}
}
barcodeDialog(
'{% trans "Check Into Location" %}',
{
headerContent: header,
extraFields: extra,
preShow: function() {
modalSetSubmitText(modal, '{% trans "Check In" %}');
modalEnable(modal, false);
},
onShow: function() {
},
onSubmit: function() {
// Called when the 'check-in' button is pressed
if (!stock_location) {
return;
}
var items = [];
item_id_list.forEach(function(pk) {
items.push({
pk: pk,
});
})
var data = {
location: stock_location.pk,
notes: $(modal + ' #notes').val(),
items: items,
};
// Send API request
inventreePut(
'{% url "api-stock-transfer" %}',
data,
{
method: 'POST',
success: function(response, status) {
// First hide the modal
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
showAlertOrCache('alert-success', response.success, true);
location.reload();
} else {
showAlertOrCache('alert-danger', '{% trans "Error transferring stock" %}', false);
}
}
}
)
},
onScan: function(response) {
updateLocationInfo(null);
if ('stocklocation' in response) {
// Barcode corresponds to a StockLocation
stock_location = response.stocklocation;
updateLocationInfo(stock_location);
modalEnable(modal, true);
} else {
// Barcode does *NOT* correspond to a StockLocation
showBarcodeMessage(
modal,
'{% trans "Barcode does not match a valid location" %}',
"warning",
);
}
}
}
)
}

View File

@ -446,6 +446,20 @@ function loadPartTable(table, url, options={}) {
}
});
columns.push({
field: 'link',
title: '{% trans "Link" %}',
formatter: function(value, row, index, field) {
return renderLink(
value, value,
{
max_length: 32,
remove_http: true,
}
);
}
});
$(table).inventreeTable({
url: url,
sortName: 'name',

View File

@ -6,6 +6,7 @@
* Requires api.js to be loaded first
*/
{% settings_value 'BARCODE_ENABLE' as barcodes %}
function stockStatusCodes() {
return [
@ -635,6 +636,9 @@ function loadStockTable(table, options) {
table,
[
'#stock-print-options',
{% if barcodes %}
'#stock-barcode-options',
{% endif %}
'#stock-options',
]
);
@ -700,6 +704,20 @@ function loadStockTable(table, options) {
printTestReports(items);
})
{% if barcodes %}
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
var items = [];
selections.forEach(function(item) {
items.push(item.pk);
})
scanItemsIntoLocation(items);
});
{% endif %}
$('#multi-item-stocktake').click(function() {
stockAdjustment('count');
});

View File

@ -1,5 +1,9 @@
{% load static %}
{% load inventree_extras %}
{% load i18n %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
<nav class="navbar navbar-xs navbar-default navbar-fixed-top ">
<div class="container-fluid">
<div class="navbar-header clearfix content-heading">
@ -46,11 +50,13 @@
</ul>
<ul class="nav navbar-nav navbar-right">
{% include "search_form.html" %}
{% if barcodes %}
<li id='navbar-barcode-li'>
<button id='barcode-scan' class='btn btn-default' title='{% trans "Scan Barcode" %}'>
<span class='fas fa-qrcode'></span>
</button>
</li>
{% endif %}
<li class='dropdown'>
<a class='dropdown-toggle' data-toggle='dropdown' href="#">
{% if not system_healthy %}
@ -61,14 +67,13 @@
{% if user.is_authenticated %}
{% if user.is_staff %}
<li><a href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
<hr>
{% endif %}
<li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
<li><a href="{% url 'logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
{% else %}
<li><a href="{% url 'login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li>
{% endif %}
<hr>
<hr>
<li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
<li id='launch-stats'><a href='#'>
{% if system_healthy %}
<span class='fas fa-server'>
@ -83,4 +88,4 @@
</ul>
</div>
</div>
</nav>
</nav>

View File

@ -1,6 +1,8 @@
{% load i18n %}
{% load inventree_extras %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
{% if owner_control.value == "True" %}
{% authorized_owners location.owner as owners %}
@ -19,6 +21,17 @@
<span class='fas fa-plus-circle'></span>
</button>
{% endif %}
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group'>
<button id='stock-barcode-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle='dropdown' title='{% trans "Barcode Actions" %}'>
<span class='fas fa-qrcode'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a href='#' id='multi-item-barcode-scan-into-location' title='{% trans "Scan to Location" %}'><span class='fas fa-sitemap'></span> {% trans "Scan to Location" %}</a></li>
</ul>
</div>
{% endif %}
<div class='btn-group'>
<button id='stock-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'>
<span class='fas fa-print'></span> <span class='caret'></span>

View File

@ -15,7 +15,7 @@ pygments==2.2.0 # Syntax highlighting
tablib==0.13.0 # Import / export data files
django-crispy-forms==1.8.1 # Form helpers
django-import-export==2.0.0 # Data import / export for admin interface
django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
django-qr-code==1.2.0 # Generate QR codes
flake8==3.8.3 # PEP checking
pep8-naming==0.11.1 # PEP naming convention extension