mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Merged changes from master
This commit is contained in:
commit
7d5571ba5b
@ -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', ['*'])
|
||||||
|
@ -131,7 +131,6 @@ $.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');
|
||||||
|
|
||||||
|
@ -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,16 +5,19 @@
|
|||||||
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:
|
||||||
@ -22,3 +25,9 @@
|
|||||||
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
|
||||||
|
Binary file not shown.
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* 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/'
|
||||||
|
|
||||||
# List all order items
|
|
||||||
response = self.doGet(url)
|
response = self.doGet(url)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
# Filter by stuff
|
self.assertEqual(response.status_code, 200)
|
||||||
response = self.doGet(url, 'status=10&part=1&supplier_part=1')
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
data = response.data
|
||||||
|
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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:
|
||||||
|
@ -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',
|
||||||
|
@ -2022,7 +2022,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
|
||||||
|
|
||||||
@ -2033,7 +2033,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
|
||||||
|
|
||||||
|
@ -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" %}
|
||||||
|
@ -256,6 +256,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 %}
|
||||||
{
|
{
|
||||||
@ -677,6 +692,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" %}',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,31 +14,32 @@
|
|||||||
</button>
|
</button>
|
||||||
{% if read_only %}
|
{% if read_only %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- 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 owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
||||||
{% if roles.stock.add %}
|
{% if roles.stock.add %}
|
||||||
<button class="btn btn-success" id='item-create'>
|
<button class="btn btn-success" id='item-create'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if roles.stock.change or roles.stock.delete %}
|
{% if roles.stock.change or roles.stock.delete %}
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if roles.stock.change %}
|
{% if roles.stock.change %}
|
||||||
<li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
|
<li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li>
|
||||||
<li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li>
|
<li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove 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-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>
|
||||||
{% endif %}
|
<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>
|
||||||
{% if roles.stock.delete %}
|
{% endif %}
|
||||||
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
|
{% if roles.stock.delete %}
|
||||||
{% endif %}
|
<li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li>
|
||||||
</ul>
|
{% endif %}
|
||||||
</div>
|
</ul>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class='filter-list' id='filter-list-stock'>
|
<div class='filter-list' id='filter-list-stock'>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user