mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into label-api
# Conflicts: # InvenTree/locale/de/LC_MESSAGES/django.po # InvenTree/locale/en/LC_MESSAGES/django.po # InvenTree/locale/es/LC_MESSAGES/django.po
This commit is contained in:
commit
0134597747
@ -11,17 +11,20 @@ database setup in this file.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import yaml
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import yaml
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def _is_true(x):
|
||||||
|
return x in [True, "True", "true", "Y", "y", "1"]
|
||||||
|
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@ -36,11 +39,14 @@ with open(cfg_filename, 'r') as cfg:
|
|||||||
|
|
||||||
# Default action is to run the system in Debug mode
|
# Default action is to run the system in Debug mode
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = CONFIG.get('debug', True)
|
DEBUG = _is_true(os.getenv("INVENTREE_DEBUG", CONFIG.get("debug", True)))
|
||||||
|
|
||||||
# Configure logging settings
|
# Configure logging settings
|
||||||
|
|
||||||
log_level = CONFIG.get('log_level', 'DEBUG').upper()
|
log_level = CONFIG.get('log_level', 'DEBUG').upper()
|
||||||
|
logging.basicConfig(
|
||||||
|
level=log_level,
|
||||||
|
format="%(asctime)s %(levelname)s %(message)s",
|
||||||
|
)
|
||||||
|
|
||||||
if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||||
log_level = 'WARNING'
|
log_level = 'WARNING'
|
||||||
@ -59,20 +65,31 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=log_level,
|
|
||||||
format='%(asctime)s %(levelname)s %(message)s',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get a logger instance for this setup file
|
# Get a logger instance for this setup file
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Read the autogenerated key-file
|
if os.getenv("INVENTREE_SECRET_KEY"):
|
||||||
key_file_name = os.path.join(BASE_DIR, 'secret_key.txt')
|
# Secret key passed in directly
|
||||||
logger.info(f'Loading SECRET_KEY from {key_file_name}')
|
SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip()
|
||||||
key_file = open(key_file_name, 'r')
|
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY")
|
||||||
|
else:
|
||||||
SECRET_KEY = key_file.read().strip()
|
# Secret key passed in by file location
|
||||||
|
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
||||||
|
if key_file:
|
||||||
|
if os.path.isfile(key_file):
|
||||||
|
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY_FILE")
|
||||||
|
else:
|
||||||
|
logger.error(f"Secret key file {key_file} not found")
|
||||||
|
exit(-1)
|
||||||
|
else:
|
||||||
|
# default secret key location
|
||||||
|
key_file = os.path.join(BASE_DIR, "secret_key.txt")
|
||||||
|
logger.info(f"SECRET_KEY loaded from {key_file}")
|
||||||
|
try:
|
||||||
|
SECRET_KEY = open(key_file, "r").read().strip()
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Couldn't load keyfile {key_file}")
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
# List of allowed hosts (default = allow all)
|
# List of allowed hosts (default = allow all)
|
||||||
ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
|
ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
|
||||||
@ -112,7 +129,7 @@ MEDIA_ROOT = os.path.abspath(CONFIG.get('media_root', os.path.join(BASE_DIR, 'me
|
|||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
logger.info("InvenTree running in DEBUG mode")
|
logger.info("InvenTree running in DEBUG mode")
|
||||||
|
|
||||||
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
||||||
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||||
|
|
||||||
@ -315,7 +332,7 @@ else:
|
|||||||
- However there may be reason to configure the DB via environmental variables
|
- However there may be reason to configure the DB via environmental variables
|
||||||
- The following code lets the user "mix and match" database configuration
|
- The following code lets the user "mix and match" database configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info("Configuring database backend:")
|
logger.info("Configuring database backend:")
|
||||||
|
|
||||||
# Extract database configuration from the config.yaml file
|
# Extract database configuration from the config.yaml file
|
||||||
@ -341,7 +358,7 @@ else:
|
|||||||
|
|
||||||
# Check that required database configuration options are specified
|
# Check that required database configuration options are specified
|
||||||
reqiured_keys = ['ENGINE', 'NAME']
|
reqiured_keys = ['ENGINE', 'NAME']
|
||||||
|
|
||||||
for key in reqiured_keys:
|
for key in reqiured_keys:
|
||||||
if key not in db_config:
|
if key not in db_config:
|
||||||
error_msg = f'Missing required database configuration value {key} in config.yaml'
|
error_msg = f'Missing required database configuration value {key} in config.yaml'
|
||||||
|
@ -131,8 +131,7 @@ $.fn.inventreeTable = function(options) {
|
|||||||
|
|
||||||
// Callback when a column is changed
|
// Callback when a column is changed
|
||||||
options.onColumnSwitch = function(field, checked) {
|
options.onColumnSwitch = function(field, checked) {
|
||||||
console.log(`${field} -> ${checked}`);
|
|
||||||
|
|
||||||
var columns = table.bootstrapTable('getVisibleColumns');
|
var columns = table.bootstrapTable('getVisibleColumns');
|
||||||
|
|
||||||
var text = visibleColumnString(columns);
|
var text = visibleColumnString(columns);
|
||||||
|
@ -160,6 +160,13 @@ class InvenTreeSetting(models.Model):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'PART_SHOW_QUANTITY_IN_FORMS': {
|
||||||
|
'name': _('Show Quantity in Forms'),
|
||||||
|
'description': _('Display available part quantity in some forms'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
'STOCK_ENABLE_EXPIRY': {
|
'STOCK_ENABLE_EXPIRY': {
|
||||||
'name': _('Stock Expiry'),
|
'name': _('Stock Expiry'),
|
||||||
'description': _('Enable stock expiry functionality'),
|
'description': _('Enable stock expiry functionality'),
|
||||||
|
@ -5,20 +5,29 @@
|
|||||||
fields:
|
fields:
|
||||||
name: ACME
|
name: ACME
|
||||||
description: A Cool Military Enterprise
|
description: A Cool Military Enterprise
|
||||||
|
|
||||||
- model: company.company
|
- model: company.company
|
||||||
pk: 2
|
pk: 2
|
||||||
fields:
|
fields:
|
||||||
name: Appel Computers
|
name: Appel Computers
|
||||||
description: Think more differenter
|
description: Think more differenter
|
||||||
|
|
||||||
- model: company.company
|
- model: company.company
|
||||||
pk: 3
|
pk: 3
|
||||||
fields:
|
fields:
|
||||||
name: Zerg Corp
|
name: Zerg Corp
|
||||||
description: We eat the competition
|
description: We eat the competition
|
||||||
|
|
||||||
- model: company.company
|
- model: company.company
|
||||||
pk: 4
|
pk: 4
|
||||||
fields:
|
fields:
|
||||||
name: A customer
|
name: A customer
|
||||||
description: A company that we sell things to!
|
description: A company that we sell things to!
|
||||||
is_customer: True
|
is_customer: True
|
||||||
|
|
||||||
|
- model: company.company
|
||||||
|
pk: 5
|
||||||
|
fields:
|
||||||
|
name: Another customer!
|
||||||
|
description: Yet another company
|
||||||
|
is_customer: True
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -80,6 +80,17 @@ class POList(generics.ListCreateAPIView):
|
|||||||
else:
|
else:
|
||||||
queryset = queryset.exclude(status__in=PurchaseOrderStatus.OPEN)
|
queryset = queryset.exclude(status__in=PurchaseOrderStatus.OPEN)
|
||||||
|
|
||||||
|
# Filter by 'overdue' status
|
||||||
|
overdue = params.get('overdue', None)
|
||||||
|
|
||||||
|
if overdue is not None:
|
||||||
|
overdue = str2bool(overdue)
|
||||||
|
|
||||||
|
if overdue:
|
||||||
|
queryset = queryset.filter(PurchaseOrder.OVERDUE_FILTER)
|
||||||
|
else:
|
||||||
|
queryset = queryset.exclude(PurchaseOrder.OVERDUE_FILTER)
|
||||||
|
|
||||||
# Special filtering for 'status' field
|
# Special filtering for 'status' field
|
||||||
status = params.get('status', None)
|
status = params.get('status', None)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
reference: '0001'
|
reference: '0001'
|
||||||
description: "Ordering some screws"
|
description: "Ordering some screws"
|
||||||
supplier: 1
|
supplier: 1
|
||||||
|
status: 10 # Pending
|
||||||
|
|
||||||
# Ordering some screws from Zerg Corp
|
# Ordering some screws from Zerg Corp
|
||||||
- model: order.purchaseorder
|
- model: order.purchaseorder
|
||||||
@ -15,6 +16,39 @@
|
|||||||
reference: '0002'
|
reference: '0002'
|
||||||
description: "Ordering some more screws"
|
description: "Ordering some more screws"
|
||||||
supplier: 3
|
supplier: 3
|
||||||
|
status: 10 # Pending
|
||||||
|
|
||||||
|
- model: order.purchaseorder
|
||||||
|
pk: 3
|
||||||
|
fields:
|
||||||
|
reference: '0003'
|
||||||
|
description: 'Another PO'
|
||||||
|
supplier: 3
|
||||||
|
status: 20 # Placed
|
||||||
|
|
||||||
|
- model: order.purchaseorder
|
||||||
|
pk: 4
|
||||||
|
fields:
|
||||||
|
reference: '0004'
|
||||||
|
description: 'Another PO'
|
||||||
|
supplier: 3
|
||||||
|
status: 20 # Placed
|
||||||
|
|
||||||
|
- model: order.purchaseorder
|
||||||
|
pk: 5
|
||||||
|
fields:
|
||||||
|
reference: '0005'
|
||||||
|
description: 'Another PO'
|
||||||
|
supplier: 3
|
||||||
|
status: 30 # Complete
|
||||||
|
|
||||||
|
- model: order.purchaseorder
|
||||||
|
pk: 6
|
||||||
|
fields:
|
||||||
|
reference: '0006'
|
||||||
|
description: 'Another PO'
|
||||||
|
supplier: 3
|
||||||
|
status: 40 # Cancelled
|
||||||
|
|
||||||
# Add some line items against PO 0001
|
# Add some line items against PO 0001
|
||||||
|
|
||||||
|
39
InvenTree/order/fixtures/sales_order.yaml
Normal file
39
InvenTree/order/fixtures/sales_order.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
- model: order.salesorder
|
||||||
|
pk: 1
|
||||||
|
fields:
|
||||||
|
reference: 'ABC123'
|
||||||
|
description: "One sales order, please"
|
||||||
|
customer: 4
|
||||||
|
status: 10 # Pending
|
||||||
|
|
||||||
|
- model: order.salesorder
|
||||||
|
pk: 2
|
||||||
|
fields:
|
||||||
|
reference: 'ABC124'
|
||||||
|
description: "One sales order, please"
|
||||||
|
customer: 4
|
||||||
|
status: 10 # Pending
|
||||||
|
|
||||||
|
- model: order.salesorder
|
||||||
|
pk: 3
|
||||||
|
fields:
|
||||||
|
reference: 'ABC125'
|
||||||
|
description: "One sales order, please"
|
||||||
|
customer: 4
|
||||||
|
status: 10 # Pending
|
||||||
|
|
||||||
|
- model: order.salesorder
|
||||||
|
pk: 4
|
||||||
|
fields:
|
||||||
|
reference: 'ABC126'
|
||||||
|
description: "One sales order, please"
|
||||||
|
customer: 5
|
||||||
|
status: 20 # Shipped
|
||||||
|
|
||||||
|
- model: order.salesorder
|
||||||
|
pk: 5
|
||||||
|
fields:
|
||||||
|
reference: 'ABC127'
|
||||||
|
description: "One sales order, please"
|
||||||
|
customer: 5
|
||||||
|
status: 60 # Returned
|
@ -94,6 +94,7 @@ class EditPurchaseOrderForm(HelperForm):
|
|||||||
self.field_prefix = {
|
self.field_prefix = {
|
||||||
'reference': 'PO',
|
'reference': 'PO',
|
||||||
'link': 'fa-link',
|
'link': 'fa-link',
|
||||||
|
'target_date': 'fa-calendar-alt',
|
||||||
}
|
}
|
||||||
|
|
||||||
self.field_placeholder = {
|
self.field_placeholder = {
|
||||||
@ -102,6 +103,10 @@ class EditPurchaseOrderForm(HelperForm):
|
|||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
target_date = DatePickerFormField(
|
||||||
|
help_text=_('Target date for order delivery. Order will be overdue after this date.'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
fields = [
|
fields = [
|
||||||
@ -109,6 +114,7 @@ class EditPurchaseOrderForm(HelperForm):
|
|||||||
'supplier',
|
'supplier',
|
||||||
'supplier_reference',
|
'supplier_reference',
|
||||||
'description',
|
'description',
|
||||||
|
'target_date',
|
||||||
'link',
|
'link',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
28
InvenTree/order/migrations/0041_auto_20210114_1728.py
Normal file
28
InvenTree/order/migrations/0041_auto_20210114_1728.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-01-14 06:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0040_salesorder_target_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='target_date',
|
||||||
|
field=models.DateField(blank=True, help_text='Expected date for order delivery. Order will be overdue after this date.', null=True, verbose_name='Target Delivery Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='complete_date',
|
||||||
|
field=models.DateField(blank=True, help_text='Date order was completed', null=True, verbose_name='Completion Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='issue_date',
|
||||||
|
field=models.DateField(blank=True, help_text='Date order was issued', null=True, verbose_name='Issue Date'),
|
||||||
|
),
|
||||||
|
]
|
@ -119,8 +119,11 @@ class PurchaseOrder(Order):
|
|||||||
supplier: Reference to the company supplying the goods in the order
|
supplier: Reference to the company supplying the goods in the order
|
||||||
supplier_reference: Optional field for supplier order reference code
|
supplier_reference: Optional field for supplier order reference code
|
||||||
received_by: User that received the goods
|
received_by: User that received the goods
|
||||||
|
target_date: Expected delivery target date for PurchaseOrder completion (optional)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
OVERDUE_FILTER = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filterByDate(queryset, min_date, max_date):
|
def filterByDate(queryset, min_date, max_date):
|
||||||
"""
|
"""
|
||||||
@ -132,7 +135,7 @@ class PurchaseOrder(Order):
|
|||||||
|
|
||||||
To be "interesting":
|
To be "interesting":
|
||||||
- A "received" order where the received date lies within the date range
|
- A "received" order where the received date lies within the date range
|
||||||
- TODO: A "pending" order where the target date lies within the date range
|
- A "pending" order where the target date lies within the date range
|
||||||
- TODO: An "overdue" order where the target date is in the past
|
- TODO: An "overdue" order where the target date is in the past
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -149,13 +152,12 @@ class PurchaseOrder(Order):
|
|||||||
# Construct a queryset for "received" orders within the range
|
# Construct a queryset for "received" orders within the range
|
||||||
received = Q(status=PurchaseOrderStatus.COMPLETE) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date)
|
received = Q(status=PurchaseOrderStatus.COMPLETE) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date)
|
||||||
|
|
||||||
# TODO - Construct a queryset for "pending" orders within the range
|
# Construct a queryset for "pending" orders within the range
|
||||||
|
pending = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
||||||
|
|
||||||
# TODO - Construct a queryset for "overdue" orders within the range
|
# TODO - Construct a queryset for "overdue" orders within the range
|
||||||
|
|
||||||
flt = received
|
queryset = queryset.filter(received | pending)
|
||||||
|
|
||||||
queryset = queryset.filter(flt)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@ -186,9 +188,23 @@ class PurchaseOrder(Order):
|
|||||||
related_name='+'
|
related_name='+'
|
||||||
)
|
)
|
||||||
|
|
||||||
issue_date = models.DateField(blank=True, null=True, help_text=_('Date order was issued'))
|
issue_date = models.DateField(
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Issue Date'),
|
||||||
|
help_text=_('Date order was issued')
|
||||||
|
)
|
||||||
|
|
||||||
complete_date = models.DateField(blank=True, null=True, help_text=_('Date order was completed'))
|
target_date = models.DateField(
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Target Delivery Date'),
|
||||||
|
help_text=_('Expected date for order delivery. Order will be overdue after this date.'),
|
||||||
|
)
|
||||||
|
|
||||||
|
complete_date = models.DateField(
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Completion Date'),
|
||||||
|
help_text=_('Date order was completed')
|
||||||
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('po-detail', kwargs={'pk': self.id})
|
return reverse('po-detail', kwargs={'pk': self.id})
|
||||||
@ -256,8 +272,24 @@ class PurchaseOrder(Order):
|
|||||||
self.complete_date = datetime.now().date()
|
self.complete_date = datetime.now().date()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def is_overdue(self):
|
||||||
|
"""
|
||||||
|
Returns True if this PurchaseOrder is "overdue"
|
||||||
|
|
||||||
|
Makes use of the OVERDUE_FILTER to avoid code duplication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = PurchaseOrder.objects.filter(pk=self.pk)
|
||||||
|
query = query.filter(PurchaseOrder.OVERDUE_FILTER)
|
||||||
|
|
||||||
|
return query.exists()
|
||||||
|
|
||||||
def can_cancel(self):
|
def can_cancel(self):
|
||||||
return self.status not in [
|
"""
|
||||||
|
A PurchaseOrder can only be cancelled under the following circumstances:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.status in [
|
||||||
PurchaseOrderStatus.PLACED,
|
PurchaseOrderStatus.PLACED,
|
||||||
PurchaseOrderStatus.PENDING
|
PurchaseOrderStatus.PENDING
|
||||||
]
|
]
|
||||||
@ -419,17 +451,13 @@ class SalesOrder(Order):
|
|||||||
"""
|
"""
|
||||||
Returns true if this SalesOrder is "overdue":
|
Returns true if this SalesOrder is "overdue":
|
||||||
|
|
||||||
- Not completed
|
Makes use of the OVERDUE_FILTER to avoid code duplication.
|
||||||
- Target date is "in the past"
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Order cannot be deemed overdue if target_date is not set
|
query = SalesOrder.objects.filter(pk=self.pk)
|
||||||
if self.target_date is None:
|
query = query.filer(SalesOrder.OVERDUE_FILTER)
|
||||||
return False
|
|
||||||
|
|
||||||
today = datetime.now().date()
|
return query.exists()
|
||||||
|
|
||||||
return self.is_pending and self.target_date < today
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_pending(self):
|
def is_pending(self):
|
||||||
|
@ -40,12 +40,24 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""
|
"""
|
||||||
Add extra information to the queryset
|
Add extra information to the queryset
|
||||||
|
|
||||||
|
- Number of liens in the PurchaseOrder
|
||||||
|
- Overdue status of the PurchaseOrder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
line_items=SubqueryCount('lines')
|
line_items=SubqueryCount('lines')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
overdue=Case(
|
||||||
|
When(
|
||||||
|
PurchaseOrder.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
|
||||||
|
),
|
||||||
|
default=Value(False, output_field=BooleanField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
|
||||||
@ -54,6 +66,8 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
|
|
||||||
|
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
|
|
||||||
@ -65,12 +79,14 @@ class POSerializer(InvenTreeModelSerializer):
|
|||||||
'description',
|
'description',
|
||||||
'line_items',
|
'line_items',
|
||||||
'link',
|
'link',
|
||||||
|
'overdue',
|
||||||
'reference',
|
'reference',
|
||||||
'supplier',
|
'supplier',
|
||||||
'supplier_detail',
|
'supplier_detail',
|
||||||
'supplier_reference',
|
'supplier_reference',
|
||||||
'status',
|
'status',
|
||||||
'status_text',
|
'status_text',
|
||||||
|
'target_date',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -26,7 +26,12 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<a href="{% url 'admin:order_purchaseorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
|
<a href="{% url 'admin:order_purchaseorder_change' order.pk %}"><span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
<h3>{% purchase_order_status_label order.status large=True %}</h3>
|
<h3>
|
||||||
|
{% purchase_order_status_label order.status large=True %}
|
||||||
|
{% if order.is_overdue %}
|
||||||
|
<span class='label label-large label-large-red'>{% trans "Overdue" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p>{{ order.description }}</p>
|
<p>{{ order.description }}</p>
|
||||||
<div class='btn-row'>
|
<div class='btn-row'>
|
||||||
@ -47,7 +52,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<span class='fas fa-check-circle'></span>
|
<span class='fas fa-check-circle'></span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING or order.status == PurchaseOrderStatus.PLACED %}
|
{% if order.can_cancel %}
|
||||||
<button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
|
<button type='button' class='btn btn-default' id='cancel-order' title='{% trans "Cancel order" %}'>
|
||||||
<span class='fas fa-times-circle icon-red'></span>
|
<span class='fas fa-times-circle icon-red'></span>
|
||||||
</button>
|
</button>
|
||||||
@ -72,7 +77,12 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Order Status" %}</td>
|
<td>{% trans "Order Status" %}</td>
|
||||||
<td>{% purchase_order_status_label order.status %}</td>
|
<td>
|
||||||
|
{% purchase_order_status_label order.status %}
|
||||||
|
{% if order.is_overdue %}
|
||||||
|
<span class='label label-red'>{% trans "Overdue" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-building'></span></td>
|
<td><span class='fas fa-building'></span></td>
|
||||||
@ -105,6 +115,13 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{{ order.issue_date }}</td>
|
<td>{{ order.issue_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if order.target_date %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-calendar-alt'></span></td>
|
||||||
|
<td>{% trans "Target Date" %}</td>
|
||||||
|
<td>{{ order.target_date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% if order.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>
|
||||||
|
@ -70,6 +70,8 @@ InvenTree | {% trans "Purchase Orders" %}
|
|||||||
|
|
||||||
if (order.complete_date) {
|
if (order.complete_date) {
|
||||||
date = order.complete_date;
|
date = order.complete_date;
|
||||||
|
} else if (order.target_date) {
|
||||||
|
date = order.target_date;
|
||||||
}
|
}
|
||||||
|
|
||||||
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
|
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
|
||||||
|
@ -2,12 +2,16 @@
|
|||||||
Tests for the Order API
|
Tests for the Order API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from .models import PurchaseOrder, SalesOrder
|
||||||
|
|
||||||
|
|
||||||
class OrderTest(APITestCase):
|
class OrderTest(APITestCase):
|
||||||
|
|
||||||
@ -18,6 +22,8 @@ class OrderTest(APITestCase):
|
|||||||
'location',
|
'location',
|
||||||
'supplier_part',
|
'supplier_part',
|
||||||
'stock',
|
'stock',
|
||||||
|
'order',
|
||||||
|
'sales_order',
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -26,21 +32,80 @@ class OrderTest(APITestCase):
|
|||||||
get_user_model().objects.create_user('testuser', 'test@testing.com', 'password')
|
get_user_model().objects.create_user('testuser', 'test@testing.com', 'password')
|
||||||
self.client.login(username='testuser', password='password')
|
self.client.login(username='testuser', password='password')
|
||||||
|
|
||||||
def doGet(self, url, options=''):
|
def doGet(self, url, data={}):
|
||||||
|
|
||||||
return self.client.get(url + "?" + options, format='json')
|
return self.client.get(url, data=data, format='json')
|
||||||
|
|
||||||
|
def doPost(self, url, data={}):
|
||||||
|
return self.client.post(url, data=data, format='json')
|
||||||
|
|
||||||
|
def filter(self, filters, count):
|
||||||
|
"""
|
||||||
|
Test API filters
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.doGet(
|
||||||
|
self.LIST_URL,
|
||||||
|
filters
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(response.data), count)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderTest(OrderTest):
|
||||||
|
"""
|
||||||
|
Tests for the PurchaseOrder API
|
||||||
|
"""
|
||||||
|
|
||||||
|
LIST_URL = reverse('api-po-list')
|
||||||
|
|
||||||
def test_po_list(self):
|
def test_po_list(self):
|
||||||
|
|
||||||
url = reverse('api-po-list')
|
|
||||||
|
|
||||||
# List all order items
|
# List *ALL* PO items
|
||||||
|
self.filter({}, 6)
|
||||||
|
|
||||||
|
# Filter by supplier
|
||||||
|
self.filter({'supplier': 1}, 1)
|
||||||
|
self.filter({'supplier': 3}, 5)
|
||||||
|
|
||||||
|
# Filter by "outstanding"
|
||||||
|
self.filter({'outstanding': True}, 4)
|
||||||
|
self.filter({'outstanding': False}, 2)
|
||||||
|
|
||||||
|
# Filter by "status"
|
||||||
|
self.filter({'status': 10}, 2)
|
||||||
|
self.filter({'status': 40}, 1)
|
||||||
|
|
||||||
|
def test_overdue(self):
|
||||||
|
"""
|
||||||
|
Test "overdue" status
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.filter({'overdue': True}, 0)
|
||||||
|
self.filter({'overdue': False}, 6)
|
||||||
|
|
||||||
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
|
order.target_date = datetime.now().date() - timedelta(days=10)
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
self.filter({'overdue': True}, 1)
|
||||||
|
self.filter({'overdue': False}, 5)
|
||||||
|
|
||||||
|
def test_po_detail(self):
|
||||||
|
|
||||||
|
url = '/api/order/po/1/'
|
||||||
|
|
||||||
response = self.doGet(url)
|
response = self.doGet(url)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Filter by stuff
|
data = response.data
|
||||||
response = self.doGet(url, 'status=10&part=1&supplier_part=1')
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(data['pk'], 1)
|
||||||
|
self.assertEqual(data['description'], 'Ordering some screws')
|
||||||
|
|
||||||
def test_po_attachments(self):
|
def test_po_attachments(self):
|
||||||
|
|
||||||
@ -50,6 +115,60 @@ class OrderTest(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderTest(OrderTest):
|
||||||
|
"""
|
||||||
|
Tests for the SalesOrder API
|
||||||
|
"""
|
||||||
|
|
||||||
|
LIST_URL = reverse('api-so-list')
|
||||||
|
|
||||||
|
def test_so_list(self):
|
||||||
|
|
||||||
|
# All orders
|
||||||
|
self.filter({}, 5)
|
||||||
|
|
||||||
|
# Filter by customer
|
||||||
|
self.filter({'customer': 4}, 3)
|
||||||
|
self.filter({'customer': 5}, 2)
|
||||||
|
|
||||||
|
# Filter by outstanding
|
||||||
|
self.filter({'outstanding': True}, 3)
|
||||||
|
self.filter({'outstanding': False}, 2)
|
||||||
|
|
||||||
|
# Filter by status
|
||||||
|
self.filter({'status': 10}, 3) # PENDING
|
||||||
|
self.filter({'status': 20}, 1) # SHIPPED
|
||||||
|
self.filter({'status': 99}, 0) # Invalid
|
||||||
|
|
||||||
|
def test_overdue(self):
|
||||||
|
"""
|
||||||
|
Test "overdue" status
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.filter({'overdue': True}, 0)
|
||||||
|
self.filter({'overdue': False}, 5)
|
||||||
|
|
||||||
|
for pk in [1, 2]:
|
||||||
|
order = SalesOrder.objects.get(pk=pk)
|
||||||
|
order.target_date = datetime.now().date() - timedelta(days=10)
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
self.filter({'overdue': True}, 2)
|
||||||
|
self.filter({'overdue': False}, 3)
|
||||||
|
|
||||||
|
def test_so_detail(self):
|
||||||
|
|
||||||
|
url = '/api/order/so/1/'
|
||||||
|
|
||||||
|
response = self.doGet(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = response.data
|
||||||
|
|
||||||
|
self.assertEqual(data['pk'], 1)
|
||||||
|
|
||||||
def test_so_attachments(self):
|
def test_so_attachments(self):
|
||||||
|
|
||||||
url = reverse('api-so-attachment-list')
|
url = reverse('api-so-attachment-list')
|
||||||
|
@ -41,7 +41,7 @@ class OrderTest(TestCase):
|
|||||||
|
|
||||||
next_ref = PurchaseOrder.getNextOrderNumber()
|
next_ref = PurchaseOrder.getNextOrderNumber()
|
||||||
|
|
||||||
self.assertEqual(next_ref, '0003')
|
self.assertEqual(next_ref, '0007')
|
||||||
|
|
||||||
def test_on_order(self):
|
def test_on_order(self):
|
||||||
""" There should be 3 separate items on order for the M2x4 LPHS part """
|
""" There should be 3 separate items on order for the M2x4 LPHS part """
|
||||||
|
@ -432,7 +432,7 @@ class PurchaseOrderCancel(AjaxUpdateView):
|
|||||||
form.add_error('confirm', _('Confirm order cancellation'))
|
form.add_error('confirm', _('Confirm order cancellation'))
|
||||||
|
|
||||||
if not order.can_cancel():
|
if not order.can_cancel():
|
||||||
form.add_error(None, _('Order cannot be cancelled as either pending or placed'))
|
form.add_error(None, _('Order cannot be cancelled'))
|
||||||
|
|
||||||
def save(self, order, form, **kwargs):
|
def save(self, order, form, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -182,6 +182,10 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
|||||||
queryset = PartAttachment.objects.all()
|
queryset = PartAttachment.objects.all()
|
||||||
serializer_class = part_serializers.PartAttachmentSerializer
|
serializer_class = part_serializers.PartAttachmentSerializer
|
||||||
|
|
||||||
|
filter_backends = [
|
||||||
|
DjangoFilterBackend,
|
||||||
|
]
|
||||||
|
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
'part',
|
'part',
|
||||||
]
|
]
|
||||||
|
@ -13,6 +13,8 @@ from mptt.fields import TreeNodeChoiceField
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
import common.models
|
||||||
|
|
||||||
from .models import Part, PartCategory, PartAttachment, PartRelated
|
from .models import Part, PartCategory, PartAttachment, PartRelated
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import PartParameterTemplate, PartParameter
|
from .models import PartParameterTemplate, PartParameter
|
||||||
@ -23,8 +25,16 @@ from .models import PartSellPriceBreak
|
|||||||
|
|
||||||
class PartModelChoiceField(forms.ModelChoiceField):
|
class PartModelChoiceField(forms.ModelChoiceField):
|
||||||
""" Extending string representation of Part instance with available stock """
|
""" Extending string representation of Part instance with available stock """
|
||||||
|
|
||||||
def label_from_instance(self, part):
|
def label_from_instance(self, part):
|
||||||
return f'{part} - {part.available_stock}'
|
|
||||||
|
label = str(part)
|
||||||
|
|
||||||
|
# Optionally display available part quantity
|
||||||
|
if common.models.InvenTreeSetting.get_setting('PART_SHOW_QUANTITY_IN_FORMS'):
|
||||||
|
label += f" - {part.available_stock}"
|
||||||
|
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
class PartImageForm(HelperForm):
|
class PartImageForm(HelperForm):
|
||||||
|
@ -1990,7 +1990,13 @@ class BomItem(models.Model):
|
|||||||
Return the available stock items for the referenced sub_part
|
Return the available stock items for the referenced sub_part
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = self.sub_part.stock_items.filter(StockModels.StockItem.IN_STOCK_FILTER).aggregate(
|
query = self.sub_part.stock_items.all()
|
||||||
|
|
||||||
|
query = query.prefetch_related([
|
||||||
|
'sub_part__stock_items',
|
||||||
|
])
|
||||||
|
|
||||||
|
query = query.filter(StockModels.StockItem.IN_STOCK_FILTER).aggregate(
|
||||||
available=Coalesce(Sum('quantity'), 0)
|
available=Coalesce(Sum('quantity'), 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
{% if category %}
|
{% if category %}
|
||||||
<h3>
|
<h3>
|
||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
{% if user.is_staff and roles.part.change %}
|
{% if user.is_staff and roles.part_category.change %}
|
||||||
<a href="{% url 'admin:part_partcategory_change' category.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
|
<a href="{% url 'admin:part_partcategory_change' category.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
@ -20,18 +20,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<div class='btn-group action-buttons'>
|
<div class='btn-group action-buttons'>
|
||||||
{% if roles.part.add %}
|
{% if roles.part_category.add %}
|
||||||
<button class='btn btn-default' id='cat-create' title='{% trans "Create new part category" %}'>
|
<button class='btn btn-default' id='cat-create' title='{% trans "Create new part category" %}'>
|
||||||
<span class='fas fa-plus-circle icon-green'/>
|
<span class='fas fa-plus-circle icon-green'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if category %}
|
{% if category %}
|
||||||
{% if roles.part.change %}
|
{% if roles.part_category.change %}
|
||||||
<button class='btn btn-default' id='cat-edit' title='{% trans "Edit part category" %}'>
|
<button class='btn btn-default' id='cat-edit' title='{% trans "Edit part category" %}'>
|
||||||
<span class='fas fa-edit icon-blue'/>
|
<span class='fas fa-edit icon-blue'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roles.part.delete %}
|
{% if roles.part_category.delete %}
|
||||||
<button class='btn btn-default' id='cat-delete' title='{% trans "Delete part category" %}'>
|
<button class='btn btn-default' id='cat-delete' title='{% trans "Delete part category" %}'>
|
||||||
<span class='fas fa-trash-alt icon-red'/>
|
<span class='fas fa-trash-alt icon-red'/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -214,9 +214,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if part.active %}
|
{% if part.active %}
|
||||||
<span class='fas fa-check-square'></span>
|
<span class='fas fa-check-circle icon-green'></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class='fas fa-times-square'></span>
|
<span class='fas fa-times-circle icon-red'></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><b>{% trans "Active" %}</b></td>
|
<td><b>{% trans "Active" %}</b></td>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
{% if roles.part.change %}
|
{% if roles.part.change %}
|
||||||
<button title='{% trans "Edit" %}' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='fas fa-edit'/></button>
|
<button title='{% trans "Edit" %}' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='fas fa-edit'/></button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roles.part.delete %}
|
{% if roles.part.change %}
|
||||||
<button title='{% trans "Delete" %}' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
|
<button title='{% trans "Delete" %}' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -231,7 +231,7 @@ class PartAttachmentDelete(AjaxDeleteView):
|
|||||||
ajax_template_name = "attachment_delete.html"
|
ajax_template_name = "attachment_delete.html"
|
||||||
context_object_name = "attachment"
|
context_object_name = "attachment"
|
||||||
|
|
||||||
role_required = 'part.delete'
|
role_required = 'part.change'
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
@ -1320,7 +1320,7 @@ class BomUpload(InvenTreeRoleMixin, FormView):
|
|||||||
# Otherwise, check to see if there is a matching IPN
|
# Otherwise, check to see if there is a matching IPN
|
||||||
try:
|
try:
|
||||||
if row['part_ipn']:
|
if row['part_ipn']:
|
||||||
part_matches = [part for part in self.allowed_parts if row['part_ipn'].lower() == part.IPN.lower()]
|
part_matches = [part for part in self.allowed_parts if part.IPN and row['part_ipn'].lower() == str(part.IPN.lower())]
|
||||||
|
|
||||||
# Check for single match
|
# Check for single match
|
||||||
if len(part_matches) == 1:
|
if len(part_matches) == 1:
|
||||||
@ -2073,7 +2073,7 @@ class PartParameterEdit(AjaxUpdateView):
|
|||||||
class PartParameterDelete(AjaxDeleteView):
|
class PartParameterDelete(AjaxDeleteView):
|
||||||
""" View for deleting a PartParameter """
|
""" View for deleting a PartParameter """
|
||||||
|
|
||||||
role_required = 'part.delete'
|
role_required = 'part.change'
|
||||||
|
|
||||||
model = PartParameter
|
model = PartParameter
|
||||||
ajax_template_name = 'part/param_delete.html'
|
ajax_template_name = 'part/param_delete.html'
|
||||||
@ -2088,7 +2088,7 @@ class CategoryDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
queryset = PartCategory.objects.all().prefetch_related('children')
|
queryset = PartCategory.objects.all().prefetch_related('children')
|
||||||
template_name = 'part/category_partlist.html'
|
template_name = 'part/category_partlist.html'
|
||||||
|
|
||||||
role_required = 'part.view'
|
role_required = ['part_category.view', 'part.view']
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
@ -2138,7 +2138,7 @@ class CategoryEdit(AjaxUpdateView):
|
|||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = _('Edit Part Category')
|
ajax_form_title = _('Edit Part Category')
|
||||||
|
|
||||||
role_required = 'part.change'
|
role_required = 'part_category.change'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
|
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
|
||||||
@ -2177,7 +2177,7 @@ class CategoryDelete(AjaxDeleteView):
|
|||||||
context_object_name = 'category'
|
context_object_name = 'category'
|
||||||
success_url = '/part/'
|
success_url = '/part/'
|
||||||
|
|
||||||
role_required = 'part.delete'
|
role_required = 'part_category.delete'
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
@ -2193,7 +2193,7 @@ class CategoryCreate(AjaxCreateView):
|
|||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
form_class = part_forms.EditCategoryForm
|
form_class = part_forms.EditCategoryForm
|
||||||
|
|
||||||
role_required = 'part.add'
|
role_required = 'part_category.add'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add extra context data to template.
|
""" Add extra context data to template.
|
||||||
@ -2233,7 +2233,7 @@ class CategoryCreate(AjaxCreateView):
|
|||||||
class CategoryParameterTemplateCreate(AjaxCreateView):
|
class CategoryParameterTemplateCreate(AjaxCreateView):
|
||||||
""" View for creating a new PartCategoryParameterTemplate """
|
""" View for creating a new PartCategoryParameterTemplate """
|
||||||
|
|
||||||
role_required = 'part.add'
|
role_required = 'part_category.change'
|
||||||
|
|
||||||
model = PartCategoryParameterTemplate
|
model = PartCategoryParameterTemplate
|
||||||
form_class = part_forms.EditCategoryParameterTemplateForm
|
form_class = part_forms.EditCategoryParameterTemplateForm
|
||||||
@ -2336,7 +2336,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView):
|
|||||||
class CategoryParameterTemplateEdit(AjaxUpdateView):
|
class CategoryParameterTemplateEdit(AjaxUpdateView):
|
||||||
""" View for editing a PartCategoryParameterTemplate """
|
""" View for editing a PartCategoryParameterTemplate """
|
||||||
|
|
||||||
role_required = 'part.change'
|
role_required = 'part_category.change'
|
||||||
|
|
||||||
model = PartCategoryParameterTemplate
|
model = PartCategoryParameterTemplate
|
||||||
form_class = part_forms.EditCategoryParameterTemplateForm
|
form_class = part_forms.EditCategoryParameterTemplateForm
|
||||||
@ -2395,7 +2395,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView):
|
|||||||
class CategoryParameterTemplateDelete(AjaxDeleteView):
|
class CategoryParameterTemplateDelete(AjaxDeleteView):
|
||||||
""" View for deleting an existing PartCategoryParameterTemplate """
|
""" View for deleting an existing PartCategoryParameterTemplate """
|
||||||
|
|
||||||
role_required = 'part.delete'
|
role_required = 'part_category.change'
|
||||||
|
|
||||||
model = PartCategoryParameterTemplate
|
model = PartCategoryParameterTemplate
|
||||||
ajax_form_title = _("Delete Category Parameter Template")
|
ajax_form_title = _("Delete Category Parameter Template")
|
||||||
@ -2554,7 +2554,7 @@ class BomItemDelete(AjaxDeleteView):
|
|||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_form_title = _('Confim BOM item deletion')
|
ajax_form_title = _('Confim BOM item deletion')
|
||||||
|
|
||||||
role_required = 'part.delete'
|
role_required = 'part.change'
|
||||||
|
|
||||||
|
|
||||||
class PartSalePriceBreakCreate(AjaxCreateView):
|
class PartSalePriceBreakCreate(AjaxCreateView):
|
||||||
|
@ -208,6 +208,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
'stale',
|
'stale',
|
||||||
'status',
|
'status',
|
||||||
'status_text',
|
'status_text',
|
||||||
|
'stocktake_date',
|
||||||
'supplier_part',
|
'supplier_part',
|
||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
'tracking_items',
|
'tracking_items',
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
{% if location %}
|
{% if location %}
|
||||||
<h3>
|
<h3>
|
||||||
{{ location.name }}
|
{{ location.name }}
|
||||||
{% if user.is_staff and roles.stock.change %}
|
{% if user.is_staff and roles.stock_location.change %}
|
||||||
<a href="{% url 'admin:stock_stocklocation_change' location.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
|
<a href="{% url 'admin:stock_stocklocation_change' location.pk %}"><span title="{% trans 'Admin view' %}" class='fas fa-user-shield'></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<p>{% trans "All stock items" %}</p>
|
<p>{% trans "All stock items" %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class='btn-group action-buttons' role='group'>
|
<div class='btn-group action-buttons' role='group'>
|
||||||
{% if roles.stock.add %}
|
{% if roles.stock_location.add %}
|
||||||
<button class='btn btn-default' id='location-create' title='{% trans "Create new stock location" %}'>
|
<button class='btn btn-default' id='location-create' title='{% trans "Create new stock location" %}'>
|
||||||
<span class='fas fa-plus-circle icon-green'/>
|
<span class='fas fa-plus-circle icon-green'/>
|
||||||
</button>
|
</button>
|
||||||
@ -41,11 +41,13 @@
|
|||||||
{% trans "Count stock" %}</a></li>
|
{% trans "Count stock" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if roles.stock_location.change %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='location-actions' title='{% trans "Location actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle="dropdown"><span class='fas fa-sitemap'></span> <span class='caret'></span></button>
|
<button id='location-actions' title='{% trans "Location actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle="dropdown"><span class='fas fa-sitemap'></span> <span class='caret'></span></button>
|
||||||
<ul class='dropdown-menu' role='menu'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
<li><a href='#' id='location-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit location" %}</a></li>
|
<li><a href='#' id='location-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit location" %}</a></li>
|
||||||
{% if roles.stock.delete %}
|
{% if roles.stock_location.delete %}
|
||||||
<li><a href='#' id='location-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete location" %}</a></li>
|
<li><a href='#' id='location-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete location" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "collapse.html" %}
|
{% extends "collapse.html" %}
|
||||||
|
|
||||||
{% if roles.stock.view %}
|
{% if roles.stock_location.view or roles.stock.view %}
|
||||||
{% block collapse_title %}
|
{% block collapse_title %}
|
||||||
Sub-Locations<span class='badge'>{{ children|length }}</span>
|
Sub-Locations<span class='badge'>{{ children|length }}</span>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -72,7 +72,7 @@ class StockLocationDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
template_name = 'stock/location.html'
|
template_name = 'stock/location.html'
|
||||||
queryset = StockLocation.objects.all()
|
queryset = StockLocation.objects.all()
|
||||||
model = StockLocation
|
model = StockLocation
|
||||||
role_required = 'stock.view'
|
role_required = ['stock_location.view', 'stock.view']
|
||||||
|
|
||||||
|
|
||||||
class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
||||||
@ -120,7 +120,7 @@ class StockLocationEdit(AjaxUpdateView):
|
|||||||
context_object_name = 'location'
|
context_object_name = 'location'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = _('Edit Stock Location')
|
ajax_form_title = _('Edit Stock Location')
|
||||||
role_required = 'stock.change'
|
role_required = 'stock_location.change'
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
""" Customize form data for StockLocation editing.
|
""" Customize form data for StockLocation editing.
|
||||||
@ -145,7 +145,7 @@ class StockLocationQRCode(QRCodeView):
|
|||||||
""" View for displaying a QR code for a StockLocation object """
|
""" View for displaying a QR code for a StockLocation object """
|
||||||
|
|
||||||
ajax_form_title = _("Stock Location QR code")
|
ajax_form_title = _("Stock Location QR code")
|
||||||
role_required = 'stock.view'
|
role_required = ['stock_location.view', 'stock.view']
|
||||||
|
|
||||||
def get_qr_data(self):
|
def get_qr_data(self):
|
||||||
""" Generate QR code data for the StockLocation """
|
""" Generate QR code data for the StockLocation """
|
||||||
@ -1274,7 +1274,7 @@ class StockLocationCreate(AjaxCreateView):
|
|||||||
context_object_name = 'location'
|
context_object_name = 'location'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = _('Create new Stock Location')
|
ajax_form_title = _('Create new Stock Location')
|
||||||
role_required = 'stock.add'
|
role_required = 'stock_location.add'
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initials = super(StockLocationCreate, self).get_initial().copy()
|
initials = super(StockLocationCreate, self).get_initial().copy()
|
||||||
@ -1634,7 +1634,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
item = form.save(commit=False)
|
item = form.save(commit=False)
|
||||||
item.user = self.request.user
|
item.user = self.request.user
|
||||||
item.save()
|
item.save(user=self.request.user)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@ -1645,7 +1645,7 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
item = form.save(commit=False)
|
item = form.save(commit=False)
|
||||||
item.user = self.request.user
|
item.user = self.request.user
|
||||||
item.save()
|
item.save(user=self.request.user)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@ -1661,7 +1661,7 @@ class StockLocationDelete(AjaxDeleteView):
|
|||||||
ajax_template_name = 'stock/location_delete.html'
|
ajax_template_name = 'stock/location_delete.html'
|
||||||
context_object_name = 'location'
|
context_object_name = 'location'
|
||||||
ajax_form_title = _('Delete Stock Location')
|
ajax_form_title = _('Delete Stock Location')
|
||||||
role_required = 'stock.delete'
|
role_required = 'stock_location.delete'
|
||||||
|
|
||||||
|
|
||||||
class StockItemDelete(AjaxDeleteView):
|
class StockItemDelete(AjaxDeleteView):
|
||||||
|
@ -35,6 +35,7 @@ InvenTree | {% trans "Index" %}
|
|||||||
{% if roles.purchase_order.view %}
|
{% if roles.purchase_order.view %}
|
||||||
{% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %}
|
{% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include "InvenTree/po_overdue.html" with collapse_id="po_overdue" %}
|
||||||
{% if roles.sales_order.view %}
|
{% if roles.sales_order.view %}
|
||||||
{% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %}
|
{% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %}
|
||||||
{% include "InvenTree/so_overdue.html" with collapse_id="so_overdue" %}
|
{% include "InvenTree/so_overdue.html" with collapse_id="so_overdue" %}
|
||||||
@ -130,6 +131,14 @@ loadPurchaseOrderTable("#po-outstanding-table", {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loadPurchaseOrderTable("#po-overdue-table", {
|
||||||
|
url: "{% url 'api-po-list' %}",
|
||||||
|
params: {
|
||||||
|
supplier_detail: true,
|
||||||
|
overdue: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loadSalesOrderTable("#so-outstanding-table", {
|
loadSalesOrderTable("#so-outstanding-table", {
|
||||||
url: "{% url 'api-so-list' %}",
|
url: "{% url 'api-so-list' %}",
|
||||||
params: {
|
params: {
|
||||||
@ -158,6 +167,7 @@ loadSalesOrderTable("#so-overdue-table", {
|
|||||||
{% include "InvenTree/index/on_load.html" with label="stock-to-build" %}
|
{% include "InvenTree/index/on_load.html" with label="stock-to-build" %}
|
||||||
|
|
||||||
{% include "InvenTree/index/on_load.html" with label="po-outstanding" %}
|
{% include "InvenTree/index/on_load.html" with label="po-outstanding" %}
|
||||||
|
{% include "InvenTree/index/on_load.html" with label="po-overdue" %}
|
||||||
{% include "InvenTree/index/on_load.html" with label="so-outstanding" %}
|
{% include "InvenTree/index/on_load.html" with label="so-outstanding" %}
|
||||||
{% include "InvenTree/index/on_load.html" with label="so-overdue" %}
|
{% include "InvenTree/index/on_load.html" with label="so-overdue" %}
|
||||||
|
|
||||||
|
15
InvenTree/templates/InvenTree/po_overdue.html
Normal file
15
InvenTree/templates/InvenTree/po_overdue.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "collapse_index.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block collapse_title %}
|
||||||
|
<span class='fas fa-calendar-times icon-header'></span>
|
||||||
|
{% trans "Overdue Purchase Orders" %}<span class='badge' id='po-overdue-count'><span class='fas fa-spin fa-spinner'></span></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block collapse_content %}
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' id='po-overdue-table'>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -18,6 +18,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %}
|
||||||
<tr><td colspan='5 '></td></tr>
|
<tr><td colspan='5 '></td></tr>
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %}
|
{% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %}
|
||||||
|
@ -255,6 +255,38 @@ function loadBomTable(table, options) {
|
|||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
'field': 'can_build',
|
||||||
|
'title': '{% trans "Can Build" %}',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var can_build = 0;
|
||||||
|
|
||||||
|
if (row.quantity > 0) {
|
||||||
|
can_build = row.sub_part_detail.stock / row.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return +can_build.toFixed(2);
|
||||||
|
},
|
||||||
|
sorter: function(valA, valB, rowA, rowB) {
|
||||||
|
// Function to sort the "can build" quantity
|
||||||
|
var cb_a = 0;
|
||||||
|
var cb_b = 0;
|
||||||
|
|
||||||
|
if (rowA.quantity > 0) {
|
||||||
|
cb_a = rowA.sub_part_detail.stock / rowA.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowB.quantity > 0) {
|
||||||
|
cb_b = rowB.sub_part_detail.stock / rowB.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cb_a > cb_b) ? 1 : -1;
|
||||||
|
},
|
||||||
|
sortable: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Part notes
|
// Part notes
|
||||||
cols.push(
|
cols.push(
|
||||||
|
@ -141,9 +141,9 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
|
||||||
field: 'reference',
|
field: 'reference',
|
||||||
title: '{% trans "Purchase Order" %}',
|
title: '{% trans "Purchase Order" %}',
|
||||||
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
@ -153,13 +153,19 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
value = `${prefix}${value}`;
|
value = `${prefix}${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderLink(value, `/order/purchase-order/${row.pk}/`);
|
var html = renderLink(value, `/order/purchase-order/${row.pk}/`);
|
||||||
|
|
||||||
|
if (row.overdue) {
|
||||||
|
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
|
||||||
field: 'supplier_detail',
|
field: 'supplier_detail',
|
||||||
title: '{% trans "Supplier" %}',
|
title: '{% trans "Supplier" %}',
|
||||||
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`);
|
return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`);
|
||||||
}
|
}
|
||||||
@ -170,27 +176,32 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
|
||||||
field: 'description',
|
field: 'description',
|
||||||
title: '{% trans "Description" %}',
|
title: '{% trans "Description" %}',
|
||||||
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
|
||||||
field: 'status',
|
field: 'status',
|
||||||
title: '{% trans "Status" %}',
|
title: '{% trans "Status" %}',
|
||||||
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
return purchaseOrderStatusDisplay(row.status, row.status_text);
|
return purchaseOrderStatusDisplay(row.status, row.status_text);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
|
||||||
field: 'creation_date',
|
field: 'creation_date',
|
||||||
title: '{% trans "Date" %}',
|
title: '{% trans "Date" %}',
|
||||||
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
field: 'target_date',
|
||||||
|
title: '{% trans "Target Date" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
field: 'line_items',
|
field: 'line_items',
|
||||||
title: '{% trans "Items" %}'
|
title: '{% trans "Items" %}',
|
||||||
|
sortable: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -6,8 +6,18 @@
|
|||||||
* Requires api.js to be loaded first
|
* Requires api.js to be loaded first
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Functions for interacting with stock management forms
|
|
||||||
*/
|
function stockStatusCodes() {
|
||||||
|
return [
|
||||||
|
{% for code in StockStatus.list %}
|
||||||
|
{
|
||||||
|
key: {{ code.key }},
|
||||||
|
text: "{{ code.value }}",
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function removeStockRow(e) {
|
function removeStockRow(e) {
|
||||||
// Remove a selected row from a stock modal form
|
// Remove a selected row from a stock modal form
|
||||||
@ -590,6 +600,11 @@ function loadStockTable(table, options) {
|
|||||||
return locationDetail(row);
|
return locationDetail(row);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'stocktake_date',
|
||||||
|
title: '{% trans "Stocktake" %}',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
|
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
|
||||||
{% if expiry %}
|
{% if expiry %}
|
||||||
{
|
{
|
||||||
@ -689,6 +704,93 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#multi-item-set-status").click(function() {
|
||||||
|
// Select and set the STATUS field for selected stock items
|
||||||
|
var selections = $("#stock-table").bootstrapTable('getSelections');
|
||||||
|
|
||||||
|
// Select stock status
|
||||||
|
var modal = '#modal-form';
|
||||||
|
|
||||||
|
var status_list = makeOptionsList(
|
||||||
|
stockStatusCodes(),
|
||||||
|
function(item) {
|
||||||
|
return item.text;
|
||||||
|
},
|
||||||
|
function (item) {
|
||||||
|
return item.key;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add an empty option at the start of the list
|
||||||
|
status_list.unshift('<option value="">---------</option>');
|
||||||
|
|
||||||
|
// Construct form
|
||||||
|
var html = `
|
||||||
|
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
||||||
|
<div class='form-group'>
|
||||||
|
<label class='control-label requiredField' for='id_status'>
|
||||||
|
{% trans "Stock Status" %}
|
||||||
|
</label>
|
||||||
|
<div class='controls'>
|
||||||
|
<select id='id_status' class='select form-control' name='label'>
|
||||||
|
${status_list}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>`;
|
||||||
|
|
||||||
|
openModal({
|
||||||
|
modal: modal,
|
||||||
|
});
|
||||||
|
|
||||||
|
modalEnable(modal, true);
|
||||||
|
modalSetTitle(modal, '{% trans "Set Stock Status" %}');
|
||||||
|
modalSetContent(modal, html);
|
||||||
|
|
||||||
|
attachSelect(modal);
|
||||||
|
|
||||||
|
modalSubmit(modal, function() {
|
||||||
|
var label = $(modal).find('#id_status');
|
||||||
|
|
||||||
|
var status_code = label.val();
|
||||||
|
|
||||||
|
closeModal(modal);
|
||||||
|
|
||||||
|
if (!status_code) {
|
||||||
|
showAlertDialog(
|
||||||
|
'{% trans "Select Status Code" %}',
|
||||||
|
'{% trans "Status code must be selected" %}'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requests = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
var url = `/api/stock/${item.pk}/`;
|
||||||
|
|
||||||
|
requests.push(
|
||||||
|
inventreePut(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
status: status_code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
success: function() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$.when.apply($, requests).then(function() {
|
||||||
|
$("#stock-table").bootstrapTable('refresh');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
$("#multi-item-delete").click(function() {
|
$("#multi-item-delete").click(function() {
|
||||||
var selections = $("#stock-table").bootstrapTable("getSelections");
|
var selections = $("#stock-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
@ -214,6 +214,10 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Outstanding" %}',
|
title: '{% trans "Outstanding" %}',
|
||||||
},
|
},
|
||||||
|
overdue: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Overdue" %}',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
<li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
|
<li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
|
||||||
<li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
|
<li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
|
||||||
<li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
|
<li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
|
||||||
|
<li><a href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roles.stock.delete %}
|
{% if roles.stock.delete %}
|
||||||
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
|
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
|
||||||
|
@ -30,6 +30,8 @@ class RuleSetInline(admin.TabularInline):
|
|||||||
max_num = len(RuleSet.RULESET_CHOICES)
|
max_num = len(RuleSet.RULESET_CHOICES)
|
||||||
min_num = 1
|
min_num = 1
|
||||||
extra = 0
|
extra = 0
|
||||||
|
# TODO: find better way to order inlines
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeGroupAdminForm(forms.ModelForm):
|
class InvenTreeGroupAdminForm(forms.ModelForm):
|
||||||
@ -87,7 +89,8 @@ class RoleGroupAdmin(admin.ModelAdmin):
|
|||||||
RuleSetInline,
|
RuleSetInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
list_display = ('name', 'admin', 'part', 'stock', 'build', 'purchase_order', 'sales_order')
|
list_display = ('name', 'admin', 'part_category', 'part', 'stock_location',
|
||||||
|
'stock_item', 'build', 'purchase_order', 'sales_order')
|
||||||
|
|
||||||
def get_rule_set(self, obj, rule_set_type):
|
def get_rule_set(self, obj, rule_set_type):
|
||||||
''' Return list of permissions for the given ruleset '''
|
''' Return list of permissions for the given ruleset '''
|
||||||
@ -130,10 +133,16 @@ class RoleGroupAdmin(admin.ModelAdmin):
|
|||||||
def admin(self, obj):
|
def admin(self, obj):
|
||||||
return self.get_rule_set(obj, 'admin')
|
return self.get_rule_set(obj, 'admin')
|
||||||
|
|
||||||
|
def part_category(self, obj):
|
||||||
|
return self.get_rule_set(obj, 'part_category')
|
||||||
|
|
||||||
def part(self, obj):
|
def part(self, obj):
|
||||||
return self.get_rule_set(obj, 'part')
|
return self.get_rule_set(obj, 'part')
|
||||||
|
|
||||||
def stock(self, obj):
|
def stock_location(self, obj):
|
||||||
|
return self.get_rule_set(obj, 'stock_location')
|
||||||
|
|
||||||
|
def stock_item(self, obj):
|
||||||
return self.get_rule_set(obj, 'stock')
|
return self.get_rule_set(obj, 'stock')
|
||||||
|
|
||||||
def build(self, obj):
|
def build(self, obj):
|
||||||
|
18
InvenTree/users/migrations/0004_auto_20210113_1909.py
Normal file
18
InvenTree/users/migrations/0004_auto_20210113_1909.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2021-01-13 19:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0003_auto_20201005_2227'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ruleset',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(choices=[('admin', 'Admin'), ('part_category', 'Part Categories'), ('part', 'Parts'), ('stock_location', 'Stock Locations'), ('stock', 'Stock Items'), ('build', 'Build Orders'), ('purchase_order', 'Purchase Orders'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
@ -25,8 +25,10 @@ class RuleSet(models.Model):
|
|||||||
|
|
||||||
RULESET_CHOICES = [
|
RULESET_CHOICES = [
|
||||||
('admin', _('Admin')),
|
('admin', _('Admin')),
|
||||||
|
('part_category', _('Part Categories')),
|
||||||
('part', _('Parts')),
|
('part', _('Parts')),
|
||||||
('stock', _('Stock')),
|
('stock_location', _('Stock Locations')),
|
||||||
|
('stock', _('Stock Items')),
|
||||||
('build', _('Build Orders')),
|
('build', _('Build Orders')),
|
||||||
('purchase_order', _('Purchase Orders')),
|
('purchase_order', _('Purchase Orders')),
|
||||||
('sales_order', _('Sales Orders')),
|
('sales_order', _('Sales Orders')),
|
||||||
@ -48,21 +50,25 @@ class RuleSet(models.Model):
|
|||||||
'authtoken_token',
|
'authtoken_token',
|
||||||
'users_ruleset',
|
'users_ruleset',
|
||||||
],
|
],
|
||||||
|
'part_category': [
|
||||||
|
'part_partcategory',
|
||||||
|
'part_partcategoryparametertemplate',
|
||||||
|
],
|
||||||
'part': [
|
'part': [
|
||||||
'part_part',
|
'part_part',
|
||||||
'part_bomitem',
|
'part_bomitem',
|
||||||
'part_partcategory',
|
|
||||||
'part_partattachment',
|
'part_partattachment',
|
||||||
'part_partsellpricebreak',
|
'part_partsellpricebreak',
|
||||||
'part_parttesttemplate',
|
'part_parttesttemplate',
|
||||||
'part_partparametertemplate',
|
'part_partparametertemplate',
|
||||||
'part_partparameter',
|
'part_partparameter',
|
||||||
'part_partrelated',
|
'part_partrelated',
|
||||||
'part_partcategoryparametertemplate',
|
],
|
||||||
|
'stock_location': [
|
||||||
|
'stock_stocklocation',
|
||||||
],
|
],
|
||||||
'stock': [
|
'stock': [
|
||||||
'stock_stockitem',
|
'stock_stockitem',
|
||||||
'stock_stocklocation',
|
|
||||||
'stock_stockitemattachment',
|
'stock_stockitemattachment',
|
||||||
'stock_stockitemtracking',
|
'stock_stockitemtracking',
|
||||||
'stock_stockitemtestresult',
|
'stock_stockitemtestresult',
|
||||||
|
Loading…
Reference in New Issue
Block a user