mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' into scheduling
This commit is contained in:
commit
a278e52443
@ -407,14 +407,16 @@ def DownloadFile(data, filename, content_type='application/text', inline=False):
|
|||||||
|
|
||||||
|
|
||||||
def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||||
""" Attempt to extract serial numbers from an input string.
|
"""
|
||||||
- Serial numbers must be integer values
|
Attempt to extract serial numbers from an input string:
|
||||||
- Serial numbers must be positive
|
|
||||||
- Serial numbers can be split by whitespace / newline / commma chars
|
Requirements:
|
||||||
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
- Serial numbers can be either strings, or integers
|
||||||
- Serial numbers can be defined as ~ for getting the next available serial number
|
- Serial numbers can be split by whitespace / newline / commma chars
|
||||||
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
||||||
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
- Serial numbers can be defined as ~ for getting the next available serial number
|
||||||
|
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
||||||
|
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
serials: input string with patterns
|
serials: input string with patterns
|
||||||
@ -428,17 +430,18 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
|||||||
if '~' in serials:
|
if '~' in serials:
|
||||||
serials = serials.replace('~', str(next_number))
|
serials = serials.replace('~', str(next_number))
|
||||||
|
|
||||||
|
# Split input string by whitespace or comma (,) characters
|
||||||
groups = re.split("[\s,]+", serials)
|
groups = re.split("[\s,]+", serials)
|
||||||
|
|
||||||
numbers = []
|
numbers = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# helpers
|
# Helper function to check for duplicated numbers
|
||||||
def number_add(n):
|
def add_sn(sn):
|
||||||
if n in numbers:
|
if sn in numbers:
|
||||||
errors.append(_('Duplicate serial: {n}').format(n=n))
|
errors.append(_('Duplicate serial: {sn}').format(sn=sn))
|
||||||
else:
|
else:
|
||||||
numbers.append(n)
|
numbers.append(sn)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
expected_quantity = int(expected_quantity)
|
expected_quantity = int(expected_quantity)
|
||||||
@ -466,7 +469,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
|||||||
|
|
||||||
if a < b:
|
if a < b:
|
||||||
for n in range(a, b + 1):
|
for n in range(a, b + 1):
|
||||||
number_add(n)
|
add_sn(n)
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
|
||||||
@ -495,21 +498,20 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
|||||||
end = start + expected_quantity
|
end = start + expected_quantity
|
||||||
|
|
||||||
for n in range(start, end):
|
for n in range(start, end):
|
||||||
number_add(n)
|
add_sn(n)
|
||||||
# no case
|
# no case
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
|
||||||
# Group should be a number
|
# At this point, we assume that the "group" is just a single serial value
|
||||||
elif group:
|
elif group:
|
||||||
# try conversion
|
|
||||||
try:
|
|
||||||
number = int(group)
|
|
||||||
except:
|
|
||||||
# seem like it is not a number
|
|
||||||
raise ValidationError(_(f"Invalid group {group}"))
|
|
||||||
|
|
||||||
number_add(number)
|
try:
|
||||||
|
# First attempt to add as an integer value
|
||||||
|
add_sn(int(group))
|
||||||
|
except (ValueError):
|
||||||
|
# As a backup, add as a string value
|
||||||
|
add_sn(group)
|
||||||
|
|
||||||
# No valid input group detected
|
# No valid input group detected
|
||||||
else:
|
else:
|
||||||
|
@ -309,6 +309,14 @@ if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover
|
|||||||
INSTALLED_APPS.append('debug_toolbar')
|
INSTALLED_APPS.append('debug_toolbar')
|
||||||
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||||
|
|
||||||
|
# InvenTree URL configuration
|
||||||
|
|
||||||
|
# Base URL for admin pages (default="admin")
|
||||||
|
INVENTREE_ADMIN_URL = get_setting(
|
||||||
|
'INVENTREE_ADMIN_URL',
|
||||||
|
CONFIG.get('admin_url', 'admin'),
|
||||||
|
)
|
||||||
|
|
||||||
ROOT_URLCONF = 'InvenTree.urls'
|
ROOT_URLCONF = 'InvenTree.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
@ -37,7 +37,6 @@ function showAlertOrCache(message, cache, options={}) {
|
|||||||
if (cache) {
|
if (cache) {
|
||||||
addCachedAlert(message, options);
|
addCachedAlert(message, options);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
showMessage(message, options);
|
showMessage(message, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,6 +81,8 @@ function showMessage(message, options={}) {
|
|||||||
|
|
||||||
var timeout = options.timeout || 5000;
|
var timeout = options.timeout || 5000;
|
||||||
|
|
||||||
|
var target = options.target || $('#alerts');
|
||||||
|
|
||||||
var details = '';
|
var details = '';
|
||||||
|
|
||||||
if (options.details) {
|
if (options.details) {
|
||||||
@ -111,7 +112,7 @@ function showMessage(message, options={}) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$('#alerts').append(html);
|
target.append(html);
|
||||||
|
|
||||||
// Remove the alert automatically after a specified period of time
|
// Remove the alert automatically after a specified period of time
|
||||||
$(`#alert-${id}`).delay(timeout).slideUp(200, function() {
|
$(`#alert-${id}`).delay(timeout).slideUp(200, function() {
|
||||||
|
@ -14,6 +14,8 @@ from django_q.monitor import Stat
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
import InvenTree.ready
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
@ -56,6 +58,12 @@ def is_email_configured():
|
|||||||
|
|
||||||
configured = True
|
configured = True
|
||||||
|
|
||||||
|
if InvenTree.ready.isInTestMode():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if InvenTree.ready.isImportingData():
|
||||||
|
return False
|
||||||
|
|
||||||
if not settings.EMAIL_HOST:
|
if not settings.EMAIL_HOST:
|
||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
@ -89,6 +97,14 @@ def check_system_health(**kwargs):
|
|||||||
|
|
||||||
result = True
|
result = True
|
||||||
|
|
||||||
|
if InvenTree.ready.isInTestMode():
|
||||||
|
# Do not perform further checks if we are running unit tests
|
||||||
|
return False
|
||||||
|
|
||||||
|
if InvenTree.ready.isImportingData():
|
||||||
|
# Do not perform further checks if we are importing data
|
||||||
|
return False
|
||||||
|
|
||||||
if not is_worker_running(**kwargs): # pragma: no cover
|
if not is_worker_running(**kwargs): # pragma: no cover
|
||||||
result = False
|
result = False
|
||||||
logger.warning(_("Background worker check failed"))
|
logger.warning(_("Background worker check failed"))
|
||||||
|
@ -4,7 +4,6 @@ Top-level URL lookup for InvenTree application.
|
|||||||
Passes URL lookup downstream to each app as required.
|
Passes URL lookup downstream to each app as required.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
@ -169,9 +168,9 @@ frontendpatterns = [
|
|||||||
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||||
|
|
||||||
# admin sites
|
# admin sites
|
||||||
url(r'^admin/error_log/', include('error_report.urls')),
|
url(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')),
|
||||||
url(r'^admin/shell/', include('django_admin_shell.urls')),
|
url(f'^{settings.INVENTREE_ADMIN_URL}/shell/', include('django_admin_shell.urls')),
|
||||||
url(r'^admin/', admin.site.urls, name='inventree-admin'),
|
url(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'),
|
||||||
|
|
||||||
# DB user sessions
|
# DB user sessions
|
||||||
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
|
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
|
||||||
|
@ -12,11 +12,14 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 26
|
INVENTREE_API_VERSION = 27
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v27 -> 2022-02-28
|
||||||
|
- Adds target_date field to individual line items for purchase orders and sales orders
|
||||||
|
|
||||||
v26 -> 2022-02-17
|
v26 -> 2022-02-17
|
||||||
- Adds API endpoint for uploading a BOM file and extracting data
|
- Adds API endpoint for uploading a BOM file and extracting data
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% trans "Target Date" %}
|
{% trans "Target Date" %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ build.target_date }}
|
{% render_date build.target_date %}
|
||||||
{% if build.is_overdue %}
|
{% if build.is_overdue %}
|
||||||
<span title='{% blocktrans with target=build.target_date %}This build was due on {{target}}{% endblocktrans %}' class='badge badge-right rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
<span title='{% blocktrans with target=build.target_date %}This build was due on {{target}}{% endblocktrans %}' class='badge badge-right rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<table class='table table-striped'>
|
<table class='table table-striped table-condensed'>
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
@ -120,19 +120,19 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
<table class='table table-striped'>
|
<table class='table table-striped table-condensed'>
|
||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Created" %}</td>
|
<td>{% trans "Created" %}</td>
|
||||||
<td>{{ build.creation_date }}</td>
|
<td>{% render_date build.creation_date %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Target Date" %}</td>
|
<td>{% trans "Target Date" %}</td>
|
||||||
{% if build.target_date %}
|
{% if build.target_date %}
|
||||||
<td>
|
<td>
|
||||||
{{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
|
{% render_date build.target_date %}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><em>{% trans "No target date set" %}</em></td>
|
<td><em>{% trans "No target date set" %}</em></td>
|
||||||
@ -142,7 +142,7 @@
|
|||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Completed" %}</td>
|
<td>{% trans "Completed" %}</td>
|
||||||
{% if build.completion_date %}
|
{% if build.completion_date %}
|
||||||
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge badge-right rounded-pill bg-dark'>{{ build.completed_by }}</span>{% endif %}</td>
|
<td>{% render_date build.completion_date %}{% if build.completed_by %}<span class='badge badge-right rounded-pill bg-dark'>{{ build.completed_by }}</span>{% endif %}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><em>{% trans "Build not complete" %}</em></td>
|
<td><em>{% trans "Build not complete" %}</em></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -443,12 +443,12 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
except self.DoesNotExist:
|
except self.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def choices(self):
|
def choices(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the available choices for this setting (or None if no choices are defined)
|
Return the available choices for this setting (or None if no choices are defined)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.__class__.get_setting_choices(self.key)
|
return self.__class__.get_setting_choices(self.key, **kwargs)
|
||||||
|
|
||||||
def valid_options(self):
|
def valid_options(self):
|
||||||
"""
|
"""
|
||||||
@ -462,6 +462,33 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return [opt[0] for opt in choices]
|
return [opt[0] for opt in choices]
|
||||||
|
|
||||||
|
def is_choice(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Check if this setting is a "choice" field
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__class__.get_setting_choices(self.key, **kwargs) is not None
|
||||||
|
|
||||||
|
def as_choice(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Render this setting as the "display" value of a choice field,
|
||||||
|
e.g. if the choices are:
|
||||||
|
[('A4', 'A4 paper'), ('A3', 'A3 paper')],
|
||||||
|
and the value is 'A4',
|
||||||
|
then display 'A4 paper'
|
||||||
|
"""
|
||||||
|
|
||||||
|
choices = self.get_setting_choices(self.key, **kwargs)
|
||||||
|
|
||||||
|
if not choices:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
for value, display in choices:
|
||||||
|
if value == self.value:
|
||||||
|
return display
|
||||||
|
|
||||||
|
return self.value
|
||||||
|
|
||||||
def is_bool(self, **kwargs):
|
def is_bool(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Check if this setting is required to be a boolean value
|
Check if this setting is required to be a boolean value
|
||||||
@ -1212,6 +1239,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'DATE_DISPLAY_FORMAT': {
|
||||||
|
'name': _('Date Format'),
|
||||||
|
'description': _('Preferred format for displaying dates'),
|
||||||
|
'default': 'YYYY-MM-DD',
|
||||||
|
'choices': [
|
||||||
|
('YYYY-MM-DD', '2022-02-22'),
|
||||||
|
('YYYY/MM/DD', '2022/22/22'),
|
||||||
|
('DD-MM-YYYY', '22-02-2022'),
|
||||||
|
('DD/MM/YYYY', '22/02/2022'),
|
||||||
|
('MM-DD-YYYY', '02-22-2022'),
|
||||||
|
('MM/DD/YYYY', '02/22/2022'),
|
||||||
|
('MMM DD YYYY', 'Feb 22 2022'),
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -274,7 +274,7 @@ class POLineItemFilter(rest_filters.FilterSet):
|
|||||||
model = models.PurchaseOrderLineItem
|
model = models.PurchaseOrderLineItem
|
||||||
fields = [
|
fields = [
|
||||||
'order',
|
'order',
|
||||||
'part'
|
'part',
|
||||||
]
|
]
|
||||||
|
|
||||||
pending = rest_filters.BooleanFilter(label='pending', method='filter_pending')
|
pending = rest_filters.BooleanFilter(label='pending', method='filter_pending')
|
||||||
@ -391,6 +391,7 @@ class POLineItemList(generics.ListCreateAPIView):
|
|||||||
'reference',
|
'reference',
|
||||||
'SKU',
|
'SKU',
|
||||||
'total_price',
|
'total_price',
|
||||||
|
'target_date',
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
@ -401,11 +402,6 @@ class POLineItemList(generics.ListCreateAPIView):
|
|||||||
'reference',
|
'reference',
|
||||||
]
|
]
|
||||||
|
|
||||||
filter_fields = [
|
|
||||||
'order',
|
|
||||||
'part'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""
|
"""
|
||||||
@ -703,6 +699,7 @@ class SOLineItemList(generics.ListCreateAPIView):
|
|||||||
'part__name',
|
'part__name',
|
||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
|
'target_date',
|
||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
23
InvenTree/order/migrations/0062_auto_20220228_0321.py
Normal file
23
InvenTree/order/migrations/0062_auto_20220228_0321.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.10 on 2022-02-28 03:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='target_date',
|
||||||
|
field=models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesorderlineitem',
|
||||||
|
name='target_date',
|
||||||
|
field=models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.2.10 on 2022-02-28 04:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0062_auto_20220228_0321'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='purchaseorderlineitem',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
]
|
@ -398,12 +398,22 @@ class PurchaseOrder(Order):
|
|||||||
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None, **kwargs):
|
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs):
|
||||||
""" Receive a line item (or partial line item) against this PO
|
"""
|
||||||
|
Receive a line item (or partial line item) against this PO
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Extract optional batch code for the new stock item
|
||||||
|
batch_code = kwargs.get('batch_code', '')
|
||||||
|
|
||||||
|
# Extract optional list of serial numbers
|
||||||
|
serials = kwargs.get('serials', None)
|
||||||
|
|
||||||
|
# Extract optional notes field
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
barcode = kwargs.get('barcode', '')
|
|
||||||
|
# Extract optional barcode field
|
||||||
|
barcode = kwargs.get('barcode', None)
|
||||||
|
|
||||||
# Prevent null values for barcode
|
# Prevent null values for barcode
|
||||||
if barcode is None:
|
if barcode is None:
|
||||||
@ -427,33 +437,45 @@ class PurchaseOrder(Order):
|
|||||||
|
|
||||||
# Create a new stock item
|
# Create a new stock item
|
||||||
if line.part and quantity > 0:
|
if line.part and quantity > 0:
|
||||||
stock = stock_models.StockItem(
|
|
||||||
part=line.part.part,
|
|
||||||
supplier_part=line.part,
|
|
||||||
location=location,
|
|
||||||
quantity=quantity,
|
|
||||||
purchase_order=self,
|
|
||||||
status=status,
|
|
||||||
purchase_price=line.purchase_price,
|
|
||||||
uid=barcode
|
|
||||||
)
|
|
||||||
|
|
||||||
stock.save(add_note=False)
|
# Determine if we should individually serialize the items, or not
|
||||||
|
if type(serials) is list and len(serials) > 0:
|
||||||
|
serialize = True
|
||||||
|
else:
|
||||||
|
serialize = False
|
||||||
|
serials = [None]
|
||||||
|
|
||||||
tracking_info = {
|
for sn in serials:
|
||||||
'status': status,
|
|
||||||
'purchaseorder': self.pk,
|
|
||||||
}
|
|
||||||
|
|
||||||
stock.add_tracking_entry(
|
stock = stock_models.StockItem(
|
||||||
StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER,
|
part=line.part.part,
|
||||||
user,
|
supplier_part=line.part,
|
||||||
notes=notes,
|
location=location,
|
||||||
deltas=tracking_info,
|
quantity=1 if serialize else quantity,
|
||||||
location=location,
|
purchase_order=self,
|
||||||
purchaseorder=self,
|
status=status,
|
||||||
quantity=quantity
|
batch=batch_code,
|
||||||
)
|
serial=sn,
|
||||||
|
purchase_price=line.purchase_price,
|
||||||
|
uid=barcode
|
||||||
|
)
|
||||||
|
|
||||||
|
stock.save(add_note=False)
|
||||||
|
|
||||||
|
tracking_info = {
|
||||||
|
'status': status,
|
||||||
|
'purchaseorder': self.pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
stock.add_tracking_entry(
|
||||||
|
StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER,
|
||||||
|
user,
|
||||||
|
notes=notes,
|
||||||
|
deltas=tracking_info,
|
||||||
|
location=location,
|
||||||
|
purchaseorder=self,
|
||||||
|
quantity=quantity
|
||||||
|
)
|
||||||
|
|
||||||
# Update the number of parts received against the particular line item
|
# Update the number of parts received against the particular line item
|
||||||
line.received += quantity
|
line.received += quantity
|
||||||
@ -794,9 +816,18 @@ class OrderLineItem(models.Model):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
quantity: Number of items
|
quantity: Number of items
|
||||||
|
reference: Reference text (e.g. customer reference) for this line item
|
||||||
note: Annotation for the item
|
note: Annotation for the item
|
||||||
|
target_date: An (optional) date for expected shipment of this line item.
|
||||||
|
"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
Query filter for determining if an individual line item is "overdue":
|
||||||
|
- Amount received is less than the required quantity
|
||||||
|
- Target date is not None
|
||||||
|
- Target date is in the past
|
||||||
|
"""
|
||||||
|
OVERDUE_FILTER = Q(received__lt=F('quantity')) & ~Q(target_date=None) & Q(target_date__lt=datetime.now().date())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -813,6 +844,12 @@ class OrderLineItem(models.Model):
|
|||||||
|
|
||||||
notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes'))
|
notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes'))
|
||||||
|
|
||||||
|
target_date = models.DateField(
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Target Date'),
|
||||||
|
help_text=_('Target shipping date for this line item'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItem(OrderLineItem):
|
class PurchaseOrderLineItem(OrderLineItem):
|
||||||
""" Model for a purchase order line item.
|
""" Model for a purchase order line item.
|
||||||
@ -824,7 +861,6 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (
|
unique_together = (
|
||||||
('order', 'part', 'quantity', 'purchase_price')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -5,12 +5,14 @@ JSON serializers for the Order API
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Case, When, Value
|
from django.db.models import Case, When, Value
|
||||||
from django.db.models import BooleanField, ExpressionWrapper, F
|
from django.db.models import BooleanField, ExpressionWrapper, F, Q
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
@ -26,7 +28,7 @@ from InvenTree.serializers import InvenTreeModelSerializer
|
|||||||
from InvenTree.serializers import InvenTreeDecimalField
|
from InvenTree.serializers import InvenTreeDecimalField
|
||||||
from InvenTree.serializers import InvenTreeMoneySerializer
|
from InvenTree.serializers import InvenTreeMoneySerializer
|
||||||
from InvenTree.serializers import ReferenceIndexingSerializerMixin
|
from InvenTree.serializers import ReferenceIndexingSerializerMixin
|
||||||
from InvenTree.status_codes import StockStatus
|
from InvenTree.status_codes import StockStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||||
|
|
||||||
import order.models
|
import order.models
|
||||||
|
|
||||||
@ -126,6 +128,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
Add some extra annotations to this queryset:
|
Add some extra annotations to this queryset:
|
||||||
|
|
||||||
- Total price = purchase_price * quantity
|
- Total price = purchase_price * quantity
|
||||||
|
- "Overdue" status (boolean field)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
@ -135,6 +138,15 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
overdue=Case(
|
||||||
|
When(
|
||||||
|
Q(order__status__in=PurchaseOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField())
|
||||||
|
),
|
||||||
|
default=Value(False, output_field=BooleanField()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -155,6 +167,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
quantity = serializers.FloatField(default=1)
|
quantity = serializers.FloatField(default=1)
|
||||||
received = serializers.FloatField(default=0)
|
received = serializers.FloatField(default=0)
|
||||||
|
|
||||||
|
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
total_price = serializers.FloatField(read_only=True)
|
total_price = serializers.FloatField(read_only=True)
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
|
||||||
@ -185,6 +199,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'notes',
|
'notes',
|
||||||
'order',
|
'order',
|
||||||
'order_detail',
|
'order_detail',
|
||||||
|
'overdue',
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
@ -194,6 +209,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'purchase_price_string',
|
'purchase_price_string',
|
||||||
'destination',
|
'destination',
|
||||||
'destination_detail',
|
'destination_detail',
|
||||||
|
'target_date',
|
||||||
'total_price',
|
'total_price',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -203,6 +219,17 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
A serializer for receiving a single purchase order line item against a purchase order
|
A serializer for receiving a single purchase order line item against a purchase order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'barcode',
|
||||||
|
'line_item',
|
||||||
|
'location',
|
||||||
|
'quantity',
|
||||||
|
'status',
|
||||||
|
'batch_code'
|
||||||
|
'serial_numbers',
|
||||||
|
]
|
||||||
|
|
||||||
line_item = serializers.PrimaryKeyRelatedField(
|
line_item = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=order.models.PurchaseOrderLineItem.objects.all(),
|
queryset=order.models.PurchaseOrderLineItem.objects.all(),
|
||||||
many=False,
|
many=False,
|
||||||
@ -241,6 +268,22 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
|
batch_code = serializers.CharField(
|
||||||
|
label=_('Batch Code'),
|
||||||
|
help_text=_('Enter batch code for incoming stock items'),
|
||||||
|
required=False,
|
||||||
|
default='',
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_numbers = serializers.CharField(
|
||||||
|
label=_('Serial Numbers'),
|
||||||
|
help_text=_('Enter serial numbers for incoming stock items'),
|
||||||
|
required=False,
|
||||||
|
default='',
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
status = serializers.ChoiceField(
|
status = serializers.ChoiceField(
|
||||||
choices=list(StockStatus.items()),
|
choices=list(StockStatus.items()),
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK,
|
||||||
@ -270,14 +313,35 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return barcode
|
return barcode
|
||||||
|
|
||||||
class Meta:
|
def validate(self, data):
|
||||||
fields = [
|
|
||||||
'barcode',
|
data = super().validate(data)
|
||||||
'line_item',
|
|
||||||
'location',
|
line_item = data['line_item']
|
||||||
'quantity',
|
quantity = data['quantity']
|
||||||
'status',
|
serial_numbers = data.get('serial_numbers', '').strip()
|
||||||
]
|
|
||||||
|
base_part = line_item.part.part
|
||||||
|
|
||||||
|
# Does the quantity need to be "integer" (for trackable parts?)
|
||||||
|
if base_part.trackable:
|
||||||
|
|
||||||
|
if Decimal(quantity) != int(quantity):
|
||||||
|
raise ValidationError({
|
||||||
|
'quantity': _('An integer quantity must be provided for trackable parts'),
|
||||||
|
})
|
||||||
|
|
||||||
|
# If serial numbers are provided
|
||||||
|
if serial_numbers:
|
||||||
|
try:
|
||||||
|
# Pass the serial numbers through to the parent serializer once validated
|
||||||
|
data['serials'] = extract_serial_numbers(serial_numbers, quantity, base_part.getLatestSerialNumberInt())
|
||||||
|
except DjangoValidationError as e:
|
||||||
|
raise ValidationError({
|
||||||
|
'serial_numbers': e.messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class POReceiveSerializer(serializers.Serializer):
|
class POReceiveSerializer(serializers.Serializer):
|
||||||
@ -366,6 +430,8 @@ class POReceiveSerializer(serializers.Serializer):
|
|||||||
request.user,
|
request.user,
|
||||||
status=item['status'],
|
status=item['status'],
|
||||||
barcode=item.get('barcode', ''),
|
barcode=item.get('barcode', ''),
|
||||||
|
batch_code=item.get('batch_code', ''),
|
||||||
|
serials=item.get('serials', None),
|
||||||
)
|
)
|
||||||
except (ValidationError, DjangoValidationError) as exc:
|
except (ValidationError, DjangoValidationError) as exc:
|
||||||
# Catch model errors and re-throw as DRF errors
|
# Catch model errors and re-throw as DRF errors
|
||||||
@ -549,6 +615,23 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
|||||||
class SOLineItemSerializer(InvenTreeModelSerializer):
|
class SOLineItemSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializer for a SalesOrderLineItem object """
|
""" Serializer for a SalesOrderLineItem object """
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def annotate_queryset(queryset):
|
||||||
|
"""
|
||||||
|
Add some extra annotations to this queryset:
|
||||||
|
|
||||||
|
- "Overdue" status (boolean field)
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
overdue=Case(
|
||||||
|
When(
|
||||||
|
Q(order__status__in=SalesOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
|
||||||
|
),
|
||||||
|
default=Value(False, output_field=BooleanField()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
@ -570,6 +653,8 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
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, location_detail=True)
|
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
||||||
|
|
||||||
|
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
quantity = InvenTreeDecimalField()
|
quantity = InvenTreeDecimalField()
|
||||||
|
|
||||||
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
||||||
@ -599,12 +684,14 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'notes',
|
'notes',
|
||||||
'order',
|
'order',
|
||||||
'order_detail',
|
'order_detail',
|
||||||
|
'overdue',
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'sale_price',
|
'sale_price',
|
||||||
'sale_price_currency',
|
'sale_price_currency',
|
||||||
'sale_price_string',
|
'sale_price_string',
|
||||||
'shipped',
|
'shipped',
|
||||||
|
'target_date',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,27 +141,27 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Created" %}</td>
|
<td>{% trans "Created" %}</td>
|
||||||
<td>{{ order.creation_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
|
<td>{% render_date order.creation_date %}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if order.issue_date %}
|
{% if order.issue_date %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Issued" %}</td>
|
<td>{% trans "Issued" %}</td>
|
||||||
<td>{{ order.issue_date }}</td>
|
<td>{% render_date order.issue_date %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if order.target_date %}
|
{% if order.target_date %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Target Date" %}</td>
|
<td>{% trans "Target Date" %}</td>
|
||||||
<td>{{ order.target_date }}</td>
|
<td>{% render_date order.target_date %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if order.status == PurchaseOrderStatus.COMPLETE %}
|
{% if order.status == PurchaseOrderStatus.COMPLETE %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Received" %}</td>
|
<td>{% trans "Received" %}</td>
|
||||||
<td>{{ order.complete_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.received_by }}</span></td>
|
<td>{% render_date order.complete_date %}<span class='badge badge-right rounded-pill bg-dark'>{{ order.received_by }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if order.responsible %}
|
{% if order.responsible %}
|
||||||
|
@ -174,6 +174,7 @@ $('#new-po-line').click(function() {
|
|||||||
value: '{{ order.supplier.currency }}',
|
value: '{{ order.supplier.currency }}',
|
||||||
{% endif %}
|
{% endif %}
|
||||||
},
|
},
|
||||||
|
target_date: {},
|
||||||
destination: {},
|
destination: {},
|
||||||
notes: {},
|
notes: {},
|
||||||
},
|
},
|
||||||
@ -210,7 +211,7 @@ $('#new-po-line').click(function() {
|
|||||||
loadPurchaseOrderLineItemTable('#po-line-table', {
|
loadPurchaseOrderLineItemTable('#po-line-table', {
|
||||||
order: {{ order.pk }},
|
order: {{ order.pk }},
|
||||||
supplier: {{ order.supplier.pk }},
|
supplier: {{ order.supplier.pk }},
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
{% if roles.purchase_order.change %}
|
||||||
allow_edit: true,
|
allow_edit: true,
|
||||||
{% else %}
|
{% else %}
|
||||||
allow_edit: false,
|
allow_edit: false,
|
||||||
|
@ -155,13 +155,13 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Created" %}</td>
|
<td>{% trans "Created" %}</td>
|
||||||
<td>{{ order.creation_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
|
<td>{% render_date order.creation_date %}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if order.target_date %}
|
{% if order.target_date %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Target Date" %}</td>
|
<td>{% trans "Target Date" %}</td>
|
||||||
<td>{{ order.target_date }}</td>
|
<td>{% render_date order.target_date %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if order.shipment_date %}
|
{% if order.shipment_date %}
|
||||||
@ -169,7 +169,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td><span class='fas fa-truck'></span></td>
|
<td><span class='fas fa-truck'></span></td>
|
||||||
<td>{% trans "Completed" %}</td>
|
<td>{% trans "Completed" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ order.shipment_date }}
|
{% render_date order.shipment_date %}
|
||||||
{% if order.shipped_by %}
|
{% if order.shipped_by %}
|
||||||
<span class='badge badge-right rounded-pill bg-dark'>{{ order.shipped_by }}</span>
|
<span class='badge badge-right rounded-pill bg-dark'>{{ order.shipped_by }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -238,6 +238,7 @@
|
|||||||
reference: {},
|
reference: {},
|
||||||
sale_price: {},
|
sale_price: {},
|
||||||
sale_price_currency: {},
|
sale_price_currency: {},
|
||||||
|
target_date: {},
|
||||||
notes: {},
|
notes: {},
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -529,6 +529,108 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-123').exists())
|
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-123').exists())
|
||||||
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
|
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
|
||||||
|
|
||||||
|
def test_batch_code(self):
|
||||||
|
"""
|
||||||
|
Test that we can supply a 'batch code' when receiving items
|
||||||
|
"""
|
||||||
|
|
||||||
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||||
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||||
|
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0)
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'line_item': 1,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'abc-123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'line_item': 2,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'xyz-789',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'location': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
n = StockItem.objects.count()
|
||||||
|
|
||||||
|
self.post(
|
||||||
|
self.url,
|
||||||
|
data,
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that two new stock items have been created!
|
||||||
|
self.assertEqual(n + 2, StockItem.objects.count())
|
||||||
|
|
||||||
|
item_1 = StockItem.objects.filter(supplier_part=line_1.part).first()
|
||||||
|
item_2 = StockItem.objects.filter(supplier_part=line_2.part).first()
|
||||||
|
|
||||||
|
self.assertEqual(item_1.batch, 'abc-123')
|
||||||
|
self.assertEqual(item_2.batch, 'xyz-789')
|
||||||
|
|
||||||
|
def test_serial_numbers(self):
|
||||||
|
"""
|
||||||
|
Test that we can supply a 'serial number' when receiving items
|
||||||
|
"""
|
||||||
|
|
||||||
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||||
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||||
|
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0)
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'line_item': 1,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'abc-123',
|
||||||
|
'serial_numbers': '100+',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'line_item': 2,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'xyz-789',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'location': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
n = StockItem.objects.count()
|
||||||
|
|
||||||
|
self.post(
|
||||||
|
self.url,
|
||||||
|
data,
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the expected number of stock items has been created
|
||||||
|
self.assertEqual(n + 11, StockItem.objects.count())
|
||||||
|
|
||||||
|
# 10 serialized stock items created for the first line item
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 10)
|
||||||
|
|
||||||
|
# Check that the correct serial numbers have been allocated
|
||||||
|
for i in range(100, 110):
|
||||||
|
item = StockItem.objects.get(serial_int=i)
|
||||||
|
self.assertEqual(item.serial, str(i))
|
||||||
|
self.assertEqual(item.quantity, 1)
|
||||||
|
self.assertEqual(item.batch, 'abc-123')
|
||||||
|
|
||||||
|
# A single stock item (quantity 10) created for the second line item
|
||||||
|
items = StockItem.objects.filter(supplier_part=line_2.part)
|
||||||
|
self.assertEqual(items.count(), 1)
|
||||||
|
|
||||||
|
item = items.first()
|
||||||
|
|
||||||
|
self.assertEqual(item.quantity, 10)
|
||||||
|
self.assertEqual(item.batch, 'xyz-789')
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderTest(OrderTest):
|
class SalesOrderTest(OrderTest):
|
||||||
"""
|
"""
|
||||||
|
@ -313,6 +313,10 @@
|
|||||||
fields: fields,
|
fields: fields,
|
||||||
groups: partGroups(),
|
groups: partGroups(),
|
||||||
title: '{% trans "Create Part" %}',
|
title: '{% trans "Create Part" %}',
|
||||||
|
reloadFormAfterSuccess: true,
|
||||||
|
persist: true,
|
||||||
|
persistMessage: '{% trans "Create another part after this one" %}',
|
||||||
|
successMessage: '{% trans "Part created successfully" %}',
|
||||||
onSuccess: function(data) {
|
onSuccess: function(data) {
|
||||||
// Follow the new part
|
// Follow the new part
|
||||||
location.href = `/part/${data.pk}/`;
|
location.href = `/part/${data.pk}/`;
|
||||||
|
@ -969,7 +969,7 @@
|
|||||||
{% if price_history %}
|
{% if price_history %}
|
||||||
var purchasepricedata = {
|
var purchasepricedata = {
|
||||||
labels: [
|
labels: [
|
||||||
{% for line in price_history %}'{{ line.date }}',{% endfor %}
|
{% for line in price_history %}'{% render_date line.date %}',{% endfor %}
|
||||||
],
|
],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
|
label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
|
||||||
@ -1082,7 +1082,7 @@
|
|||||||
{% if sale_history %}
|
{% if sale_history %}
|
||||||
var salepricedata = {
|
var salepricedata = {
|
||||||
labels: [
|
labels: [
|
||||||
{% for line in sale_history %}'{{ line.date }}',{% endfor %}
|
{% for line in sale_history %}'{% render_date line.date %}',{% endfor %}
|
||||||
],
|
],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',
|
label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}',
|
||||||
|
@ -312,7 +312,7 @@
|
|||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Creation Date" %}</td>
|
<td>{% trans "Creation Date" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ part.creation_date }}
|
{% render_date part.creation_date %}
|
||||||
{% if part.creation_user %}
|
{% if part.creation_user %}
|
||||||
<span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span>
|
<span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -5,6 +5,7 @@ This module provides template tags for extra functionality,
|
|||||||
over and above the built-in Django tags.
|
over and above the built-in Django tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -43,6 +44,52 @@ def define(value, *args, **kwargs):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def render_date(context, date_object):
|
||||||
|
"""
|
||||||
|
Renders a date according to the preference of the provided user
|
||||||
|
|
||||||
|
Note that the user preference is stored using the formatting adopted by moment.js,
|
||||||
|
which differs from the python formatting!
|
||||||
|
"""
|
||||||
|
|
||||||
|
if date_object is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if type(date_object) == str:
|
||||||
|
# If a string is passed, first convert it to a datetime
|
||||||
|
date_object = date.fromisoformat(date_object)
|
||||||
|
|
||||||
|
# We may have already pre-cached the date format by calling this already!
|
||||||
|
user_date_format = context.get('user_date_format', None)
|
||||||
|
|
||||||
|
if user_date_format is None:
|
||||||
|
|
||||||
|
user = context.get('user', None)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# User is specified - look for their date display preference
|
||||||
|
user_date_format = InvenTreeUserSetting.get_setting('DATE_DISPLAY_FORMAT', user=user)
|
||||||
|
else:
|
||||||
|
user_date_format = 'YYYY-MM-DD'
|
||||||
|
|
||||||
|
# Convert the format string to Pythonic equivalent
|
||||||
|
replacements = [
|
||||||
|
('YYYY', '%Y'),
|
||||||
|
('MMM', '%b'),
|
||||||
|
('MM', '%m'),
|
||||||
|
('DD', '%d'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for o, n in replacements:
|
||||||
|
user_date_format = user_date_format.replace(o, n)
|
||||||
|
|
||||||
|
# Update the context cache
|
||||||
|
context['user_date_format'] = user_date_format
|
||||||
|
|
||||||
|
return date_object.strftime(user_date_format)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def decimal(x, *args, **kwargs):
|
def decimal(x, *args, **kwargs):
|
||||||
""" Simplified rendering of a decimal number """
|
""" Simplified rendering of a decimal number """
|
||||||
|
@ -5,11 +5,13 @@ import logging
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from maintenance_mode.core import set_maintenance_mode
|
from maintenance_mode.core import set_maintenance_mode
|
||||||
|
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
from plugin.helpers import check_git_version, log_error
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
@ -34,3 +36,8 @@ class PluginAppConfig(AppConfig):
|
|||||||
# drop out of maintenance
|
# drop out of maintenance
|
||||||
# makes sure we did not have an error in reloading and maintenance is still active
|
# makes sure we did not have an error in reloading and maintenance is still active
|
||||||
set_maintenance_mode(False)
|
set_maintenance_mode(False)
|
||||||
|
|
||||||
|
# check git version
|
||||||
|
registry.git_is_modern = check_git_version()
|
||||||
|
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
|
||||||
|
log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
|
||||||
|
@ -94,21 +94,46 @@ def get_git_log(path):
|
|||||||
"""
|
"""
|
||||||
Get dict with info of the last commit to file named in path
|
Get dict with info of the last commit to file named in path
|
||||||
"""
|
"""
|
||||||
path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:]
|
from plugin import registry
|
||||||
command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path]
|
|
||||||
output = None
|
output = None
|
||||||
try:
|
if registry.git_is_modern:
|
||||||
output = str(subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')[1:-1]
|
path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:]
|
||||||
if output:
|
command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path]
|
||||||
output = output.split('\n')
|
try:
|
||||||
except subprocess.CalledProcessError: # pragma: no cover
|
output = str(subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')[1:-1]
|
||||||
pass
|
if output:
|
||||||
|
output = output.split('\n')
|
||||||
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
output = 7 * [''] # pragma: no cover
|
output = 7 * [''] # pragma: no cover
|
||||||
|
|
||||||
return {'hash': output[0], 'author': output[1], 'mail': output[2], 'date': output[3], 'message': output[4], 'verified': output[5], 'key': output[6]}
|
return {'hash': output[0], 'author': output[1], 'mail': output[2], 'date': output[3], 'message': output[4], 'verified': output[5], 'key': output[6]}
|
||||||
|
|
||||||
|
|
||||||
|
def check_git_version():
|
||||||
|
"""returns if the current git version supports modern features"""
|
||||||
|
|
||||||
|
# get version string
|
||||||
|
try:
|
||||||
|
output = str(subprocess.check_output(['git', '--version'], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')
|
||||||
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
|
return False
|
||||||
|
|
||||||
|
# process version string
|
||||||
|
try:
|
||||||
|
version = output[12:-1].split(".")
|
||||||
|
if len(version) > 1 and version[0] == '2':
|
||||||
|
if len(version) > 2 and int(version[1]) >= 22:
|
||||||
|
return True
|
||||||
|
except ValueError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class GitStatus:
|
class GitStatus:
|
||||||
"""
|
"""
|
||||||
Class for resolving git gpg singing state
|
Class for resolving git gpg singing state
|
||||||
|
@ -52,6 +52,7 @@ class PluginsRegistry:
|
|||||||
# flags
|
# flags
|
||||||
self.is_loading = False
|
self.is_loading = False
|
||||||
self.apps_loading = True # Marks if apps were reloaded yet
|
self.apps_loading = True # Marks if apps were reloaded yet
|
||||||
|
self.git_is_modern = True # Is a modern version of git available
|
||||||
|
|
||||||
# integration specific
|
# integration specific
|
||||||
self.installed_apps = [] # Holds all added plugin_paths
|
self.installed_apps = [] # Holds all added plugin_paths
|
||||||
|
@ -120,13 +120,13 @@ content: "v{{report_revision}} - {{ date.isoformat }}";
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Issued" %}</th>
|
<th>{% trans "Issued" %}</th>
|
||||||
<td>{{ build.creation_date }}</td>
|
<td>{% render_date build.creation_date %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Target Date" %}</th>
|
<th>{% trans "Target Date" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if build.target_date %}
|
{% if build.target_date %}
|
||||||
{{ build.target_date }}
|
{% render_date build.target_date %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<em>Not specified</em>
|
<em>Not specified</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -63,6 +63,43 @@ class StockLocation(InvenTreeTree):
|
|||||||
help_text=_('Select Owner'),
|
help_text=_('Select Owner'),
|
||||||
related_name='stock_locations')
|
related_name='stock_locations')
|
||||||
|
|
||||||
|
def get_location_owner(self):
|
||||||
|
"""
|
||||||
|
Get the closest "owner" for this location.
|
||||||
|
|
||||||
|
Start at this location, and traverse "up" the location tree until we find an owner
|
||||||
|
"""
|
||||||
|
|
||||||
|
for loc in self.get_ancestors(include_self=True, ascending=True):
|
||||||
|
if loc.owner is not None:
|
||||||
|
return loc.owner
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_ownership(self, user):
|
||||||
|
"""
|
||||||
|
Check if the user "owns" (is one of the owners of) the location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Superuser accounts automatically "own" everything
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ownership_enabled = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
|
||||||
|
if not ownership_enabled:
|
||||||
|
# Location ownership function is not enabled, so return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
owner = self.get_location_owner()
|
||||||
|
|
||||||
|
if owner is None:
|
||||||
|
# No owner set, for this location or any location above
|
||||||
|
# So, no ownership checks to perform!
|
||||||
|
return True
|
||||||
|
|
||||||
|
return user in owner.get_related_owners(include_group=True)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
@ -614,6 +651,48 @@ class StockItem(MPTTModel):
|
|||||||
help_text=_('Select Owner'),
|
help_text=_('Select Owner'),
|
||||||
related_name='stock_items')
|
related_name='stock_items')
|
||||||
|
|
||||||
|
def get_item_owner(self):
|
||||||
|
"""
|
||||||
|
Return the closest "owner" for this StockItem.
|
||||||
|
|
||||||
|
- If the item has an owner set, return that
|
||||||
|
- If the item is "in stock", check the StockLocation
|
||||||
|
- Otherwise, return None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.owner is not None:
|
||||||
|
return self.owner
|
||||||
|
|
||||||
|
if self.in_stock and self.location is not None:
|
||||||
|
loc_owner = self.location.get_location_owner()
|
||||||
|
|
||||||
|
if loc_owner:
|
||||||
|
return loc_owner
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_ownership(self, user):
|
||||||
|
"""
|
||||||
|
Check if the user "owns" (or is one of the owners of) the item
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Superuser accounts automatically "own" everything
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ownership_enabled = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
|
||||||
|
if not ownership_enabled:
|
||||||
|
# Location ownership function is not enabled, so return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
owner = self.get_item_owner()
|
||||||
|
|
||||||
|
if owner is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return user in owner.get_related_owners(include_group=True)
|
||||||
|
|
||||||
def is_stale(self):
|
def is_stale(self):
|
||||||
"""
|
"""
|
||||||
Returns True if this Stock item is "stale".
|
Returns True if this Stock item is "stale".
|
||||||
|
@ -18,18 +18,11 @@
|
|||||||
<h4>{% trans "Stock Tracking Information" %}</h4>
|
<h4>{% trans "Stock Tracking Information" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
{% if user_owns_item and roles.stock.change and not item.is_building %}
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
{% endif %}
|
|
||||||
<!-- Check permissions and owner -->
|
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners %}
|
|
||||||
{% if roles.stock.change and not item.is_building %}
|
|
||||||
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>
|
<button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Entry" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Entry" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,14 +59,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- Stock adjustment menu -->
|
<!-- Stock adjustment menu -->
|
||||||
<!-- Check permissions and owner -->
|
{% if user_owns_item %}
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
|
||||||
{% if roles.stock.change and not item.is_building %}
|
{% if roles.stock.change and not item.is_building %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
<button id='stock-actions' title='{% trans "Stock adjustment actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
|
||||||
@ -194,7 +187,7 @@
|
|||||||
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
|
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
|
||||||
<td>{% trans "Expiry Date" %}</td>
|
<td>{% trans "Expiry Date" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ item.expiry_date }}
|
{% render_date item.expiry_date %}
|
||||||
{% if item.is_expired %}
|
{% if item.is_expired %}
|
||||||
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger badge-right'>{% trans "Expired" %}</span>
|
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger badge-right'>{% trans "Expired" %}</span>
|
||||||
{% elif item.is_stale %}
|
{% elif item.is_stale %}
|
||||||
@ -212,31 +205,15 @@
|
|||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Last Stocktake" %}</td>
|
<td>{% trans "Last Stocktake" %}</td>
|
||||||
{% if item.stocktake_date %}
|
{% if item.stocktake_date %}
|
||||||
<td>{{ item.stocktake_date }} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
|
<td>{% render_date item.stocktake_date %} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><em>{% trans "No stocktake performed" %}</em></td>
|
<td><em>{% trans "No stocktake performed" %}</em></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class='info-messages'>
|
<div class='info-messages'>
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners item.owner as owners %}
|
|
||||||
|
|
||||||
{% if not user in owners and not user.is_superuser %}
|
|
||||||
<div class='alert alert-block alert-info'>
|
|
||||||
{% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}<br>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if item.is_building %}
|
{% if item.is_building %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% trans "This stock item is in production and cannot be edited." %}<br>
|
{% trans "This stock item is in production and cannot be edited." %}<br>
|
||||||
@ -419,11 +396,18 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.owner %}
|
{% if ownership_enabled and item_owner %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-users'></span></td>
|
<td><span class='fas fa-users'></span></td>
|
||||||
<td>{% trans "Owner" %}</td>
|
<td>{% trans "Owner" %}</td>
|
||||||
<td>{{ item.owner }}</td>
|
<td>
|
||||||
|
{{ item_owner }}
|
||||||
|
{% if not user_owns_item %}
|
||||||
|
<span class='badge rounded-pill bg-warning badge-right' title='{% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}'>
|
||||||
|
{% trans "Read only" %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
|
|
||||||
<!-- Admin view -->
|
<!-- Admin view -->
|
||||||
{% if location and user.is_staff and roles.stock_location.change %}
|
{% if location and user.is_staff and roles.stock_location.change %}
|
||||||
{% url 'admin:stock_stocklocation_change' location.pk as url %}
|
{% url 'admin:stock_stocklocation_change' location.pk as url %}
|
||||||
@ -38,7 +39,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- Check permissions and owner -->
|
<!-- Check permissions and owner -->
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
{% if user_owns_location %}
|
||||||
{% if roles.stock.change %}
|
{% if roles.stock.change %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
|
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
|
||||||
@ -74,13 +75,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser or not location %}
|
{% if user_owns_location and roles.stock_location.add %}
|
||||||
{% if roles.stock_location.add %}
|
|
||||||
<button class='btn btn-success' id='location-create' type='button' title='{% trans "Create new stock location" %}'>
|
<button class='btn btn-success' id='location-create' type='button' title='{% trans "Create new stock location" %}'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Location" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Location" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block details_left %}
|
{% block details_left %}
|
||||||
@ -106,23 +105,23 @@
|
|||||||
<td><em>{% trans "Top level stock location" %}</em></td>
|
<td><em>{% trans "Top level stock location" %}</em></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if ownership_enabled and location_owner %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-users'></span></td>
|
||||||
|
<td>{% trans "Location Owner" %}</td>
|
||||||
|
<td>
|
||||||
|
{{ location_owner }}
|
||||||
|
{% if not user_owns_location %}
|
||||||
|
<span class='badge rounded-pill bg-warning badge-right' title='{% trans "You are not in the list of owners of this location. This stock location cannot be edited." %}'>
|
||||||
|
{% trans "Read only" %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% endblock details_left %}
|
{% endblock details_left %}
|
||||||
|
|
||||||
{% block details_below %}
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
|
||||||
{% if owner_control.value == "True" %}
|
|
||||||
{% authorized_owners location.owner as owners %}
|
|
||||||
|
|
||||||
{% if location and not user in owners and not user.is_superuser %}
|
|
||||||
<div class='alert alert-block alert-info'>
|
|
||||||
{% trans "You are not in the list of owners of this location. This stock location cannot be edited." %}<br>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock details_below %}
|
|
||||||
|
|
||||||
{% block details_right %}
|
{% block details_right %}
|
||||||
{% if location %}
|
{% if location %}
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
|
@ -63,6 +63,11 @@ class StockIndex(InvenTreeRoleMixin, ListView):
|
|||||||
context['loc_count'] = StockLocation.objects.count()
|
context['loc_count'] = StockLocation.objects.count()
|
||||||
context['stock_count'] = StockItem.objects.count()
|
context['stock_count'] = StockItem.objects.count()
|
||||||
|
|
||||||
|
# No 'ownership' checks are necessary for the top-level StockLocation view
|
||||||
|
context['user_owns_location'] = True
|
||||||
|
context['location_owner'] = None
|
||||||
|
context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +81,16 @@ class StockLocationDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
queryset = StockLocation.objects.all()
|
queryset = StockLocation.objects.all()
|
||||||
model = StockLocation
|
model = StockLocation
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
context['location_owner'] = context['location'].get_location_owner()
|
||||||
|
context['user_owns_location'] = context['location'].check_ownership(self.request.user)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
@ -126,6 +141,10 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
# We only support integer serial number progression
|
# We only support integer serial number progression
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
|
data['item_owner'] = self.object.get_item_owner()
|
||||||
|
data['user_owns_item'] = self.object.check_ownership(self.request.user)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ plugin.author }}</td>
|
<td>{{ plugin.author }}</td>
|
||||||
<td>{{ plugin.pub_date }}</td>
|
<td>{% render_date plugin.pub_date %}</td>
|
||||||
<td>{% if plugin.version %}{{ plugin.version }}{% endif %}</td>
|
<td>{% if plugin.version %}{{ plugin.version }}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Date" %}</td>
|
<td>{% trans "Date" %}</td>
|
||||||
<td>{{ plugin.pub_date }}{% include "clip.html" %}</td>
|
<td>{% render_date plugin.pub_date %}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
@ -101,7 +101,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Commit Date" %}</td><td>{{ plugin.package.date }}{% include "clip.html" %}</td>
|
<td>{% trans "Commit Date" %}</td><td>{% render_date plugin.package.date %}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-code-branch'></span></td>
|
<td><span class='fas fa-code-branch'></span></td>
|
||||||
|
@ -28,7 +28,11 @@
|
|||||||
<div id='setting-{{ setting.pk }}'>
|
<div id='setting-{{ setting.pk }}'>
|
||||||
<span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'>
|
<span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'>
|
||||||
{% if setting.value %}
|
{% if setting.value %}
|
||||||
|
{% if setting.is_choice %}
|
||||||
|
<strong>{{ setting.as_choice }}</strong>
|
||||||
|
{% else %}
|
||||||
<strong>{{ setting.value }}</strong>
|
<strong>{{ setting.value }}</strong>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<em style='color: #855;'>{% trans "No value set" %}</em>
|
<em style='color: #855;'>{% trans "No value set" %}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% include "InvenTree/settings/setting.html" with key="STICKY_HEADER" icon="fa-bars" user_setting=True %}
|
{% include "InvenTree/settings/setting.html" with key="STICKY_HEADER" icon="fa-bars" user_setting=True %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="DATE_DISPLAY_FORMAT" icon="fa-calendar-alt" user_setting=True %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="FORMS_CLOSE_USING_ESCAPE" icon="fa-window-close" user_setting=True %}
|
{% include "InvenTree/settings/setting.html" with key="FORMS_CLOSE_USING_ESCAPE" icon="fa-window-close" user_setting=True %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %}
|
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
{% if commit_date %}
|
{% if commit_date %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-calendar-alt'></span></td>
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
<td>{% trans "Commit Date" %}</td><td>{{ commit_date }}{% include "clip.html" %}</td>
|
<td>{% trans "Commit Date" %}</td><td>{% render_date commit_date %}{% include "clip.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
clearEvents,
|
clearEvents,
|
||||||
endDate,
|
endDate,
|
||||||
startDate,
|
startDate,
|
||||||
|
renderDate,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,3 +33,33 @@ function clearEvents(calendar) {
|
|||||||
event.remove();
|
event.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Render the provided date in the user-specified format.
|
||||||
|
*
|
||||||
|
* The provided "date" variable is a string, nominally ISO format e.g. 2022-02-22
|
||||||
|
* The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function renderDate(date, options={}) {
|
||||||
|
|
||||||
|
if (!date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fmt = user_settings.DATE_DISPLAY_FORMAT || 'YYYY-MM-DD';
|
||||||
|
|
||||||
|
if (options.showTime) {
|
||||||
|
fmt += ' HH:mm';
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = moment(date);
|
||||||
|
|
||||||
|
if (m.isValid()) {
|
||||||
|
return m.format(fmt);
|
||||||
|
} else {
|
||||||
|
// Invalid input string, simply return provided value
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -40,12 +40,15 @@ function editSetting(pk, options={}) {
|
|||||||
url = `/api/settings/user/${pk}/`;
|
url = `/api/settings/user/${pk}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reload_required = false;
|
||||||
|
|
||||||
// First, read the settings object from the server
|
// First, read the settings object from the server
|
||||||
inventreeGet(url, {}, {
|
inventreeGet(url, {}, {
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
|
|
||||||
if (response.choices && response.choices.length > 0) {
|
if (response.choices && response.choices.length > 0) {
|
||||||
response.type = 'choice';
|
response.type = 'choice';
|
||||||
|
reload_required = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the field
|
// Construct the field
|
||||||
@ -89,7 +92,9 @@ function editSetting(pk, options={}) {
|
|||||||
|
|
||||||
var setting = response.key;
|
var setting = response.key;
|
||||||
|
|
||||||
if (response.type == 'boolean') {
|
if (reload_required) {
|
||||||
|
location.reload();
|
||||||
|
} else if (response.type == 'boolean') {
|
||||||
var enabled = response.value.toString().toLowerCase() == 'true';
|
var enabled = response.value.toString().toLowerCase() == 'true';
|
||||||
$(`#setting-value-${setting}`).prop('checked', enabled);
|
$(`#setting-value-${setting}`).prop('checked', enabled);
|
||||||
} else {
|
} else {
|
||||||
|
@ -165,6 +165,9 @@ function loadAttachmentTable(url, options) {
|
|||||||
{
|
{
|
||||||
field: 'upload_date',
|
field: 'upload_date',
|
||||||
title: '{% trans "Upload Date" %}',
|
title: '{% trans "Upload Date" %}',
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
|
@ -1961,6 +1961,9 @@ function loadBuildTable(table, options) {
|
|||||||
field: 'creation_date',
|
field: 'creation_date',
|
||||||
title: '{% trans "Created" %}',
|
title: '{% trans "Created" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'issued_by',
|
field: 'issued_by',
|
||||||
@ -1990,11 +1993,17 @@ function loadBuildTable(table, options) {
|
|||||||
field: 'target_date',
|
field: 'target_date',
|
||||||
title: '{% trans "Target Date" %}',
|
title: '{% trans "Target Date" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'completion_date',
|
field: 'completion_date',
|
||||||
title: '{% trans "Completion Date" %}',
|
title: '{% trans "Completion Date" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -542,6 +542,11 @@ function constructFormBody(fields, options) {
|
|||||||
insertConfirmButton(options);
|
insertConfirmButton(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert "persist" button (if required)
|
||||||
|
if (options.persist) {
|
||||||
|
insertPersistButton(options);
|
||||||
|
}
|
||||||
|
|
||||||
// Display the modal
|
// Display the modal
|
||||||
$(modal).modal('show');
|
$(modal).modal('show');
|
||||||
|
|
||||||
@ -616,6 +621,22 @@ function insertConfirmButton(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Add a checkbox to select if the modal will stay open after success */
|
||||||
|
function insertPersistButton(options) {
|
||||||
|
|
||||||
|
var message = options.persistMessage || '{% trans "Keep this form open" %}';
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="modal-persist">
|
||||||
|
<label class="form-check-label" for="modal-persist">${message}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$(options.modal).find('#modal-footer-buttons').append(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Extract all specified form values as a single object
|
* Extract all specified form values as a single object
|
||||||
*/
|
*/
|
||||||
@ -934,19 +955,40 @@ function getFormFieldValue(name, field={}, options={}) {
|
|||||||
*/
|
*/
|
||||||
function handleFormSuccess(response, options) {
|
function handleFormSuccess(response, options) {
|
||||||
|
|
||||||
// Close the modal
|
|
||||||
if (!options.preventClose) {
|
|
||||||
// Note: The modal will be deleted automatically after closing
|
|
||||||
$(options.modal).modal('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display any required messages
|
// Display any required messages
|
||||||
// Should we show alerts immediately or cache them?
|
// Should we show alerts immediately or cache them?
|
||||||
var cache = (options.follow && response.url) || options.redirect || options.reload;
|
var cache = (options.follow && response.url) || options.redirect || options.reload;
|
||||||
|
|
||||||
|
// Should the form "persist"?
|
||||||
|
var persist = false;
|
||||||
|
|
||||||
|
if (options.persist && options.modal) {
|
||||||
|
// Determine if this form should "persist", or be dismissed?
|
||||||
|
var chk = $(options.modal).find('#modal-persist');
|
||||||
|
|
||||||
|
persist = chk.exists() && chk.prop('checked');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persist) {
|
||||||
|
cache = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg_target = null;
|
||||||
|
|
||||||
|
if (persist) {
|
||||||
|
// If the modal is persistant, the target for any messages should be the modal!
|
||||||
|
msg_target = $(options.modal).find('#pre-form-content');
|
||||||
|
}
|
||||||
|
|
||||||
// Display any messages
|
// Display any messages
|
||||||
if (response && (response.success || options.successMessage)) {
|
if (response && (response.success || options.successMessage)) {
|
||||||
showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'});
|
showAlertOrCache(
|
||||||
|
response.success || options.successMessage,
|
||||||
|
cache,
|
||||||
|
{
|
||||||
|
style: 'success',
|
||||||
|
target: msg_target,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.info) {
|
if (response && response.info) {
|
||||||
@ -961,20 +1003,41 @@ function handleFormSuccess(response, options) {
|
|||||||
showAlertOrCache(response.danger, cache, {style: 'danger'});
|
showAlertOrCache(response.danger, cache, {style: 'danger'});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.onSuccess) {
|
if (persist) {
|
||||||
// Callback function
|
// Instead of closing the form and going somewhere else,
|
||||||
options.onSuccess(response, options);
|
// reload (empty) the form so the user can input more data
|
||||||
}
|
|
||||||
|
|
||||||
if (options.follow && response.url) {
|
// Reset the status of the "submit" button
|
||||||
// Follow the returned URL
|
if (options.modal) {
|
||||||
window.location.href = response.url;
|
$(options.modal).find('#modal-form-submit').prop('disabled', false);
|
||||||
} else if (options.reload) {
|
}
|
||||||
// Reload the current page
|
|
||||||
location.reload();
|
// Remove any error flags from the form
|
||||||
} else if (options.redirect) {
|
clearFormErrors(options);
|
||||||
// Redirect to a specified URL
|
|
||||||
window.location.href = options.redirect;
|
} else {
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
if (!options.preventClose) {
|
||||||
|
// Note: The modal will be deleted automatically after closing
|
||||||
|
$(options.modal).modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.onSuccess) {
|
||||||
|
// Callback function
|
||||||
|
options.onSuccess(response, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.follow && response.url) {
|
||||||
|
// Follow the returned URL
|
||||||
|
window.location.href = response.url;
|
||||||
|
} else if (options.reload) {
|
||||||
|
// Reload the current page
|
||||||
|
location.reload();
|
||||||
|
} else if (options.redirect) {
|
||||||
|
// Redirect to a specified URL
|
||||||
|
window.location.href = options.redirect;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -989,6 +1052,8 @@ function clearFormErrors(options={}) {
|
|||||||
// Remove the individual error messages
|
// Remove the individual error messages
|
||||||
$(options.modal).find('.form-error-message').remove();
|
$(options.modal).find('.form-error-message').remove();
|
||||||
|
|
||||||
|
$(options.modal).find('.modal-content').removeClass('modal-error');
|
||||||
|
|
||||||
// Remove the "has error" class
|
// Remove the "has error" class
|
||||||
$(options.modal).find('.form-field-error').removeClass('form-field-error');
|
$(options.modal).find('.form-field-error').removeClass('form-field-error');
|
||||||
|
|
||||||
@ -1884,7 +1949,7 @@ function getFieldName(name, options={}) {
|
|||||||
* - Field description (help text)
|
* - Field description (help text)
|
||||||
* - Field errors
|
* - Field errors
|
||||||
*/
|
*/
|
||||||
function constructField(name, parameters, options) {
|
function constructField(name, parameters, options={}) {
|
||||||
|
|
||||||
var html = '';
|
var html = '';
|
||||||
|
|
||||||
@ -1976,7 +2041,7 @@ function constructField(name, parameters, options) {
|
|||||||
html += `<div class='controls'>`;
|
html += `<div class='controls'>`;
|
||||||
|
|
||||||
// Does this input deserve "extra" decorators?
|
// Does this input deserve "extra" decorators?
|
||||||
var extra = parameters.prefix != null;
|
var extra = (parameters.icon != null) || (parameters.prefix != null) || (parameters.prefixRaw != null);
|
||||||
|
|
||||||
// Some fields can have 'clear' inputs associated with them
|
// Some fields can have 'clear' inputs associated with them
|
||||||
if (!parameters.required && !parameters.read_only) {
|
if (!parameters.required && !parameters.read_only) {
|
||||||
@ -2001,6 +2066,10 @@ function constructField(name, parameters, options) {
|
|||||||
|
|
||||||
if (parameters.prefix) {
|
if (parameters.prefix) {
|
||||||
html += `<span class='input-group-text'>${parameters.prefix}</span>`;
|
html += `<span class='input-group-text'>${parameters.prefix}</span>`;
|
||||||
|
} else if (parameters.prefixRaw) {
|
||||||
|
html += parameters.prefixRaw;
|
||||||
|
} else if (parameters.icon) {
|
||||||
|
html += `<span class='input-group-text'><span class='fas ${parameters.icon}'></span></span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2147,6 +2216,10 @@ function constructInputOptions(name, classes, type, parameters, options={}) {
|
|||||||
|
|
||||||
opts.push(`type='${type}'`);
|
opts.push(`type='${type}'`);
|
||||||
|
|
||||||
|
if (parameters.title || parameters.help_text) {
|
||||||
|
opts.push(`title='${parameters.title || parameters.help_text}'`);
|
||||||
|
}
|
||||||
|
|
||||||
// Read only?
|
// Read only?
|
||||||
if (parameters.read_only) {
|
if (parameters.read_only) {
|
||||||
opts.push(`readonly=''`);
|
opts.push(`readonly=''`);
|
||||||
@ -2192,11 +2265,6 @@ function constructInputOptions(name, classes, type, parameters, options={}) {
|
|||||||
opts.push(`required=''`);
|
opts.push(`required=''`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom mouseover title?
|
|
||||||
if (parameters.title != null) {
|
|
||||||
opts.push(`title='${parameters.title}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder?
|
// Placeholder?
|
||||||
if (parameters.placeholder != null) {
|
if (parameters.placeholder != null) {
|
||||||
opts.push(`placeholder='${parameters.placeholder}'`);
|
opts.push(`placeholder='${parameters.placeholder}'`);
|
||||||
|
@ -116,6 +116,10 @@ function makeIconButton(icon, cls, pk, title, options={}) {
|
|||||||
extraProps += `disabled='true' `;
|
extraProps += `disabled='true' `;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.collapseTarget) {
|
||||||
|
extraProps += `data-bs-toggle='collapse' href='#${options.collapseTarget}'`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`;
|
html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`;
|
||||||
html += `<span class='fas ${icon}'></span>`;
|
html += `<span class='fas ${icon}'></span>`;
|
||||||
html += `</button>`;
|
html += `</button>`;
|
||||||
|
@ -476,6 +476,19 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
quantity = 0;
|
quantity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepend toggles to the quantity input
|
||||||
|
var toggle_batch = `
|
||||||
|
<span class='input-group-text' title='{% trans "Add batch code" %}' data-bs-toggle='collapse' href='#div-batch-${pk}'>
|
||||||
|
<span class='fas fa-layer-group'></span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
var toggle_serials = `
|
||||||
|
<span class='input-group-text' title='{% trans "Add serial numbers" %}' data-bs-toggle='collapse' href='#div-serials-${pk}'>
|
||||||
|
<span class='fas fa-hashtag'></span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
// Quantity to Receive
|
// Quantity to Receive
|
||||||
var quantity_input = constructField(
|
var quantity_input = constructField(
|
||||||
`items_quantity_${pk}`,
|
`items_quantity_${pk}`,
|
||||||
@ -491,6 +504,36 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add in options for "batch code" and "serial numbers"
|
||||||
|
var batch_input = constructField(
|
||||||
|
`items_batch_code_${pk}`,
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
label: '{% trans "Batch Code" %}',
|
||||||
|
help_text: '{% trans "Enter batch code for incoming stock items" %}',
|
||||||
|
prefixRaw: toggle_batch,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var sn_input = constructField(
|
||||||
|
`items_serial_numbers_${pk}`,
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
label: '{% trans "Serial Numbers" %}',
|
||||||
|
help_text: '{% trans "Enter serial numbers for incoming stock items" %}',
|
||||||
|
prefixRaw: toggle_serials,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hidden inputs below the "quantity" field
|
||||||
|
var quantity_input_group = `${quantity_input}<div class='collapse' id='div-batch-${pk}'>${batch_input}</div>`;
|
||||||
|
|
||||||
|
if (line_item.part_detail.trackable) {
|
||||||
|
quantity_input_group += `<div class='collapse' id='div-serials-${pk}'>${sn_input}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Construct list of StockItem status codes
|
// Construct list of StockItem status codes
|
||||||
var choices = [];
|
var choices = [];
|
||||||
|
|
||||||
@ -528,16 +571,38 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Button to remove the row
|
// Button to remove the row
|
||||||
var delete_button = `<div class='btn-group float-right' role='group'>`;
|
var buttons = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
delete_button += makeIconButton(
|
buttons += makeIconButton(
|
||||||
|
'fa-layer-group',
|
||||||
|
'button-row-add-batch',
|
||||||
|
pk,
|
||||||
|
'{% trans "Add batch code" %}',
|
||||||
|
{
|
||||||
|
collapseTarget: `div-batch-${pk}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (line_item.part_detail.trackable) {
|
||||||
|
buttons += makeIconButton(
|
||||||
|
'fa-hashtag',
|
||||||
|
'button-row-add-serials',
|
||||||
|
pk,
|
||||||
|
'{% trans "Add serial numbers" %}',
|
||||||
|
{
|
||||||
|
collapseTarget: `div-serials-${pk}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons += makeIconButton(
|
||||||
'fa-times icon-red',
|
'fa-times icon-red',
|
||||||
'button-row-remove',
|
'button-row-remove',
|
||||||
pk,
|
pk,
|
||||||
'{% trans "Remove row" %}',
|
'{% trans "Remove row" %}',
|
||||||
);
|
);
|
||||||
|
|
||||||
delete_button += '</div>';
|
buttons += '</div>';
|
||||||
|
|
||||||
var html = `
|
var html = `
|
||||||
<tr id='receive_row_${pk}' class='stock-receive-row'>
|
<tr id='receive_row_${pk}' class='stock-receive-row'>
|
||||||
@ -554,7 +619,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
${line_item.received}
|
${line_item.received}
|
||||||
</td>
|
</td>
|
||||||
<td id='quantity_${pk}'>
|
<td id='quantity_${pk}'>
|
||||||
${quantity_input}
|
${quantity_input_group}
|
||||||
</td>
|
</td>
|
||||||
<td id='status_${pk}'>
|
<td id='status_${pk}'>
|
||||||
${status_input}
|
${status_input}
|
||||||
@ -563,7 +628,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
${destination_input}
|
${destination_input}
|
||||||
</td>
|
</td>
|
||||||
<td id='actions_${pk}'>
|
<td id='actions_${pk}'>
|
||||||
${delete_button}
|
${buttons}
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
@ -587,7 +652,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
<th>{% trans "Order Code" %}</th>
|
<th>{% trans "Order Code" %}</th>
|
||||||
<th>{% trans "Ordered" %}</th>
|
<th>{% trans "Ordered" %}</th>
|
||||||
<th>{% trans "Received" %}</th>
|
<th>{% trans "Received" %}</th>
|
||||||
<th style='min-width: 50px;'>{% trans "Receive" %}</th>
|
<th style='min-width: 50px;'>{% trans "Quantity to Receive" %}</th>
|
||||||
<th style='min-width: 150px;'>{% trans "Status" %}</th>
|
<th style='min-width: 150px;'>{% trans "Status" %}</th>
|
||||||
<th style='min-width: 300px;'>{% trans "Destination" %}</th>
|
<th style='min-width: 300px;'>{% trans "Destination" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -678,13 +743,23 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
var location = getFormFieldValue(`items_location_${pk}`, {}, opts);
|
var location = getFormFieldValue(`items_location_${pk}`, {}, opts);
|
||||||
|
|
||||||
if (quantity != null) {
|
if (quantity != null) {
|
||||||
data.items.push({
|
|
||||||
|
var line = {
|
||||||
line_item: pk,
|
line_item: pk,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
status: status,
|
status: status,
|
||||||
location: location,
|
location: location,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (getFormFieldElement(`items_batch_code_${pk}`).exists()) {
|
||||||
|
line.batch_code = getFormFieldValue(`items_batch_code_${pk}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getFormFieldElement(`items_serial_numbers_${pk}`).exists()) {
|
||||||
|
line.serial_numbers = getFormFieldValue(`items_serial_numbers_${pk}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.items.push(line);
|
||||||
item_pk_values.push(pk);
|
item_pk_values.push(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -848,11 +923,17 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
field: 'creation_date',
|
field: 'creation_date',
|
||||||
title: '{% trans "Date" %}',
|
title: '{% trans "Date" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'target_date',
|
field: 'target_date',
|
||||||
title: '{% trans "Target Date" %}',
|
title: '{% trans "Target Date" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'line_items',
|
field: 'line_items',
|
||||||
@ -930,6 +1011,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
reference: {},
|
reference: {},
|
||||||
purchase_price: {},
|
purchase_price: {},
|
||||||
purchase_price_currency: {},
|
purchase_price_currency: {},
|
||||||
|
target_date: {},
|
||||||
destination: {},
|
destination: {},
|
||||||
notes: {},
|
notes: {},
|
||||||
},
|
},
|
||||||
@ -971,7 +1053,11 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
success: function() {
|
success: function() {
|
||||||
|
// Reload the line item table
|
||||||
$(table).bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
|
|
||||||
|
// Reload the "received stock" table
|
||||||
|
$('#stock-table').bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -1111,6 +1197,28 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
return formatter.format(total);
|
return formatter.format(total);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
field: 'target_date',
|
||||||
|
switchable: true,
|
||||||
|
title: '{% trans "Target Date" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (row.target_date) {
|
||||||
|
var html = renderDate(row.target_date);
|
||||||
|
|
||||||
|
if (row.overdue) {
|
||||||
|
html += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
|
||||||
|
} else if (row.order_detail && row.order_detail.target_date) {
|
||||||
|
return `<em>${renderDate(row.order_detail.target_date)}</em>`;
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
sortable: false,
|
sortable: false,
|
||||||
field: 'received',
|
field: 'received',
|
||||||
@ -1157,15 +1265,15 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
|
|
||||||
var pk = row.pk;
|
var pk = row.pk;
|
||||||
|
|
||||||
|
if (options.allow_receive && row.received < row.quantity) {
|
||||||
|
html += makeIconButton('fa-sign-in-alt icon-green', 'button-line-receive', pk, '{% trans "Receive line item" %}');
|
||||||
|
}
|
||||||
|
|
||||||
if (options.allow_edit) {
|
if (options.allow_edit) {
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
|
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
|
||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
|
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.allow_receive && row.received < row.quantity) {
|
|
||||||
html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}');
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
@ -1269,16 +1377,25 @@ function loadSalesOrderTable(table, options) {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'creation_date',
|
field: 'creation_date',
|
||||||
title: '{% trans "Creation Date" %}',
|
title: '{% trans "Creation Date" %}',
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'target_date',
|
field: 'target_date',
|
||||||
title: '{% trans "Target Date" %}',
|
title: '{% trans "Target Date" %}',
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'shipment_date',
|
field: 'shipment_date',
|
||||||
title: '{% trans "Shipment Date" %}',
|
title: '{% trans "Shipment Date" %}',
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
@ -1430,9 +1547,9 @@ function loadSalesOrderShipmentTable(table, options={}) {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
if (value) {
|
if (value) {
|
||||||
return value;
|
return renderDate(value);
|
||||||
} else {
|
} else {
|
||||||
return '{% trans "Not shipped" %}';
|
return '<em>{% trans "Not shipped" %}</em>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2208,6 +2325,28 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
|||||||
return formatter.format(total);
|
return formatter.format(total);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'target_date',
|
||||||
|
title: '{% trans "Target Date" %}',
|
||||||
|
sortable: true,
|
||||||
|
switchable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (row.target_date) {
|
||||||
|
var html = renderDate(row.target_date);
|
||||||
|
|
||||||
|
if (row.overdue) {
|
||||||
|
html += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
|
||||||
|
} else if (row.order_detail && row.order_detail.target_date) {
|
||||||
|
return `<em>${renderDate(row.order_detail.target_date)}</em>`;
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
if (pending) {
|
if (pending) {
|
||||||
@ -2351,6 +2490,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
|||||||
reference: {},
|
reference: {},
|
||||||
sale_price: {},
|
sale_price: {},
|
||||||
sale_price_currency: {},
|
sale_price_currency: {},
|
||||||
|
target_date: {},
|
||||||
notes: {},
|
notes: {},
|
||||||
},
|
},
|
||||||
title: '{% trans "Edit Line Item" %}',
|
title: '{% trans "Edit Line Item" %}',
|
||||||
|
@ -905,6 +905,28 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
|
|||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: '{% trans "Quantity" %}',
|
title: '{% trans "Quantity" %}',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'target_date',
|
||||||
|
title: '{% trans "Target Date" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (row.target_date) {
|
||||||
|
var html = row.target_date;
|
||||||
|
|
||||||
|
if (row.overdue) {
|
||||||
|
html += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
|
||||||
|
} else if (row.order_detail && row.order_detail.target_date) {
|
||||||
|
return `<em>${row.order_detail.target_date}</em>`;
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'received',
|
field: 'received',
|
||||||
title: '{% trans "Received" %}',
|
title: '{% trans "Received" %}',
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
modalSetContent,
|
modalSetContent,
|
||||||
modalSetTitle,
|
modalSetTitle,
|
||||||
modalSubmit,
|
modalSubmit,
|
||||||
moment,
|
|
||||||
openModal,
|
openModal,
|
||||||
printStockItemLabels,
|
printStockItemLabels,
|
||||||
printTestReports,
|
printTestReports,
|
||||||
@ -1820,6 +1819,9 @@ function loadStockTable(table, options) {
|
|||||||
col = {
|
col = {
|
||||||
field: 'stocktake_date',
|
field: 'stocktake_date',
|
||||||
title: '{% trans "Stocktake" %}',
|
title: '{% trans "Stocktake" %}',
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options.params.ordering) {
|
if (!options.params.ordering) {
|
||||||
@ -1833,6 +1835,9 @@ function loadStockTable(table, options) {
|
|||||||
title: '{% trans "Expiry Date" %}',
|
title: '{% trans "Expiry Date" %}',
|
||||||
visible: global_settings.STOCK_ENABLE_EXPIRY,
|
visible: global_settings.STOCK_ENABLE_EXPIRY,
|
||||||
switchable: global_settings.STOCK_ENABLE_EXPIRY,
|
switchable: global_settings.STOCK_ENABLE_EXPIRY,
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options.params.ordering) {
|
if (!options.params.ordering) {
|
||||||
@ -1844,6 +1849,9 @@ function loadStockTable(table, options) {
|
|||||||
col = {
|
col = {
|
||||||
field: 'updated',
|
field: 'updated',
|
||||||
title: '{% trans "Last Updated" %}',
|
title: '{% trans "Last Updated" %}',
|
||||||
|
formatter: function(value) {
|
||||||
|
return renderDate(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options.params.ordering) {
|
if (!options.params.ordering) {
|
||||||
@ -2649,14 +2657,7 @@ function loadStockTrackingTable(table, options) {
|
|||||||
title: '{% trans "Date" %}',
|
title: '{% trans "Date" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value) {
|
formatter: function(value) {
|
||||||
var m = moment(value);
|
return renderDate(value, {showTime: true});
|
||||||
|
|
||||||
if (m.isValid()) {
|
|
||||||
var html = m.format('dddd MMMM Do YYYY'); // + '<br>' + m.format('h:mm a');
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '<i>{% trans "Invalid date" %}</i>';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
|
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% if user.is_staff and not demo %}
|
{% if user.is_staff and not demo %}
|
||||||
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
|
<li><a class='dropdown-item' href="{% url 'admin:index' %}"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
|
<li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
|
||||||
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
|
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
InvenTree-Version: {% inventree_version %}
|
InvenTree-Version: {% inventree_version %}
|
||||||
Django Version: {% django_version %}
|
Django Version: {% django_version %}
|
||||||
{% inventree_commit_hash as hash %}{% if hash %}Commit Hash: {{ hash }}{% endif %}
|
{% inventree_commit_hash as hash %}{% if hash %}Commit Hash: {{ hash }}{% endif %}
|
||||||
{% inventree_commit_date as commit_date %}{% if commit_date %}Commit Date: {{ commit_date }}{% endif %}
|
{% inventree_commit_date as commit_date %}{% if commit_date %}Commit Date: {% render_date commit_date %}{% endif %}
|
||||||
Database: {% inventree_db_engine %}
|
Database: {% inventree_db_engine %}
|
||||||
Debug-Mode: {% inventree_in_debug_mode %}
|
Debug-Mode: {% inventree_in_debug_mode %}
|
||||||
Deployed using Docker: {% inventree_docker_mode %}
|
Deployed using Docker: {% inventree_docker_mode %}
|
@ -37,6 +37,9 @@ InvenTree is supported by a [companion mobile app](https://inventree.readthedocs
|
|||||||
|
|
||||||
- [**Download InvenTree from the Apple App Store**](https://apps.apple.com/au/app/inventree/id1581731101#?platform=iphone)
|
- [**Download InvenTree from the Apple App Store**](https://apps.apple.com/au/app/inventree/id1581731101#?platform=iphone)
|
||||||
|
|
||||||
|
# Deploy to DigitalOcean
|
||||||
|
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue-ghost.svg)](https://marketplace.digitalocean.com/apps/inventree?refcode=d6172576d014)
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
|
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
|
||||||
|
Loading…
Reference in New Issue
Block a user