Merge branch 'inventree:master' into plugin-mark-sample

This commit is contained in:
Matthias Mair 2022-04-19 18:24:21 +02:00 committed by GitHub
commit 544a590b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 20506 additions and 11625 deletions

View File

@ -0,0 +1,149 @@
"""
InvenTree API version information
"""
# InvenTree API version
INVENTREE_API_VERSION = 40
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v40 -> 2022-04-19
- Adds ability to filter StockItem list by "tracked" parameter
- This checks the serial number or batch code fields
v39 -> 2022-04-18
- Adds ability to filter StockItem list by "has_batch" parameter
v38 -> 2022-04-14 : https://github.com/inventree/InvenTree/pull/2828
- Adds the ability to include stock test results for "installed items"
v37 -> 2022-04-07 : https://github.com/inventree/InvenTree/pull/2806
- Adds extra stock availability information to the BomItem serializer
v36 -> 2022-04-03
- Adds ability to filter part list endpoint by unallocated_stock argument
v35 -> 2022-04-01 : https://github.com/inventree/InvenTree/pull/2797
- Adds stock allocation information to the Part API
- Adds calculated field for "unallocated_quantity"
v34 -> 2022-03-25
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
v33 -> 2022-03-24
- Adds "plugins_enabled" information to root API endpoint
v32 -> 2022-03-19
- Adds "parameters" detail to Part API endpoint (use &parameters=true)
- Adds ability to filter PartParameterTemplate API by Part instance
- Adds ability to filter PartParameterTemplate API by PartCategory instance
v31 -> 2022-03-14
- Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints
v30 -> 2022-03-09
- Adds "exclude_location" field to BuildAutoAllocation API endpoint
- Allows BuildItem API endpoint to be filtered by BomItem relation
v29 -> 2022-03-08
- Adds "scheduling" endpoint for predicted stock scheduling information
v28 -> 2022-03-04
- Adds an API endpoint for auto allocation of stock items against a build order
- Ref: https://github.com/inventree/InvenTree/pull/2713
v27 -> 2022-02-28
- Adds target_date field to individual line items for purchase orders and sales orders
v26 -> 2022-02-17
- Adds API endpoint for uploading a BOM file and extracting data
v25 -> 2022-02-17
- Adds ability to filter "part" list endpoint by "in_bom_for" argument
v24 -> 2022-02-10
- Adds API endpoint for deleting (cancelling) build order outputs
v23 -> 2022-02-02
- Adds API endpoints for managing plugin classes
- Adds API endpoints for managing plugin settings
v22 -> 2021-12-20
- Adds API endpoint to "merge" multiple stock items
v21 -> 2021-12-04
- Adds support for multiple "Shipments" against a SalesOrder
- Refactors process for stock allocation against a SalesOrder
v20 -> 2021-12-03
- Adds ability to filter POLineItem endpoint by "base_part"
- Adds optional "order_detail" to POLineItem list endpoint
v19 -> 2021-12-02
- Adds the ability to filter the StockItem API by "part_tree"
- Returns only stock items which match a particular part.tree_id field
v18 -> 2021-11-15
- Adds the ability to filter BomItem API by "uses" field
- This returns a list of all BomItems which "use" the specified part
- Includes inherited BomItem objects
v17 -> 2021-11-09
- Adds API endpoints for GLOBAL and USER settings objects
- Ref: https://github.com/inventree/InvenTree/pull/2275
v16 -> 2021-10-17
- Adds API endpoint for completing build order outputs
v15 -> 2021-10-06
- Adds detail endpoint for SalesOrderAllocation model
- Allows use of the API forms interface for adjusting SalesOrderAllocation objects
v14 -> 2021-10-05
- Stock adjustment actions API is improved, using native DRF serializer support
- However adjustment actions now only support 'pk' as a lookup field
v13 -> 2021-10-05
- Adds API endpoint to allocate stock items against a BuildOrder
- Updates StockItem API with improved filtering against BomItem data
v12 -> 2021-09-07
- Adds API endpoint to receive stock items against a PurchaseOrder
v11 -> 2021-08-26
- Adds "units" field to PartBriefSerializer
- This allows units to be introspected from the "part_detail" field in the StockItem serializer
v10 -> 2021-08-23
- Adds "purchase_price_currency" to StockItem serializer
- Adds "purchase_price_string" to StockItem serializer
- Purchase price is now writable for StockItem serializer
v9 -> 2021-08-09
- Adds "price_string" to part pricing serializers
v8 -> 2021-07-19
- Refactors the API interface for SupplierPart and ManufacturerPart models
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
v7 -> 2021-07-03
- Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716
- API OPTIONS endpoints provide comprehensive field metedata
- Multiple new API endpoints added for database models
v6 -> 2021-06-23
- Part and Company images can now be directly uploaded via the REST API
v5 -> 2021-06-21
- Adds API interface for manufacturer part parameters
v4 -> 2021-06-01
- BOM items can now accept "variant stock" to be assigned against them
- Many slight API tweaks were needed to get this to work properly!
v3 -> 2021-05-22:
- The updated StockItem "history tracking" now uses a different interface
"""

View File

@ -667,6 +667,7 @@ LANGUAGES = [
('en', _('English')),
('es', _('Spanish')),
('es-mx', _('Spanish (Mexican)')),
('fa', _('Farsi / Persian')),
('fr', _('French')),
('he', _('Hebrew')),
('hu', _('Hungarian')),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -255,6 +255,9 @@ class StockHistoryCode(StatusCode):
# Stock merging operations
MERGED_STOCK_ITEMS = 45
# Convert stock item to variant
CONVERTED_TO_VARIANT = 48
# Build order codes
BUILD_OUTPUT_CREATED = 50
BUILD_OUTPUT_COMPLETED = 55
@ -294,6 +297,8 @@ class StockHistoryCode(StatusCode):
MERGED_STOCK_ITEMS: _('Merged stock items'),
CONVERTED_TO_VARIANT: _('Converted to variant'),
SENT_TO_CUSTOMER: _('Sent to customer'),
RETURNED_FROM_CUSTOMER: _('Returned from customer'),

View File

@ -72,7 +72,7 @@ class ViewTests(TestCase):
"""
# Change this number as more javascript files are added to the index page
N_SCRIPT_FILES = 38
N_SCRIPT_FILES = 39
content = self.get_index_page()

View File

@ -1,4 +1,5 @@
""" Version information for InvenTree.
"""
Version information for InvenTree.
Provides information on the current InvenTree version
"""
@ -8,141 +9,11 @@ import re
import common.models
from InvenTree.api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.7.0 dev"
# InvenTree API version
INVENTREE_API_VERSION = 36
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v36 -> 2022-04-03
- Adds ability to filter part list endpoint by unallocated_stock argument
v35 -> 2022-04-01 : https://github.com/inventree/InvenTree/pull/2797
- Adds stock allocation information to the Part API
- Adds calculated field for "unallocated_quantity"
v34 -> 2022-03-25
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
v33 -> 2022-03-24
- Adds "plugins_enabled" information to root API endpoint
v32 -> 2022-03-19
- Adds "parameters" detail to Part API endpoint (use &parameters=true)
- Adds ability to filter PartParameterTemplate API by Part instance
- Adds ability to filter PartParameterTemplate API by PartCategory instance
v31 -> 2022-03-14
- Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints
v30 -> 2022-03-09
- Adds "exclude_location" field to BuildAutoAllocation API endpoint
- Allows BuildItem API endpoint to be filtered by BomItem relation
v29 -> 2022-03-08
- Adds "scheduling" endpoint for predicted stock scheduling information
v28 -> 2022-03-04
- Adds an API endpoint for auto allocation of stock items against a build order
- Ref: https://github.com/inventree/InvenTree/pull/2713
v27 -> 2022-02-28
- Adds target_date field to individual line items for purchase orders and sales orders
v26 -> 2022-02-17
- Adds API endpoint for uploading a BOM file and extracting data
v25 -> 2022-02-17
- Adds ability to filter "part" list endpoint by "in_bom_for" argument
v24 -> 2022-02-10
- Adds API endpoint for deleting (cancelling) build order outputs
v23 -> 2022-02-02
- Adds API endpoints for managing plugin classes
- Adds API endpoints for managing plugin settings
v22 -> 2021-12-20
- Adds API endpoint to "merge" multiple stock items
v21 -> 2021-12-04
- Adds support for multiple "Shipments" against a SalesOrder
- Refactors process for stock allocation against a SalesOrder
v20 -> 2021-12-03
- Adds ability to filter POLineItem endpoint by "base_part"
- Adds optional "order_detail" to POLineItem list endpoint
v19 -> 2021-12-02
- Adds the ability to filter the StockItem API by "part_tree"
- Returns only stock items which match a particular part.tree_id field
v18 -> 2021-11-15
- Adds the ability to filter BomItem API by "uses" field
- This returns a list of all BomItems which "use" the specified part
- Includes inherited BomItem objects
v17 -> 2021-11-09
- Adds API endpoints for GLOBAL and USER settings objects
- Ref: https://github.com/inventree/InvenTree/pull/2275
v16 -> 2021-10-17
- Adds API endpoint for completing build order outputs
v15 -> 2021-10-06
- Adds detail endpoint for SalesOrderAllocation model
- Allows use of the API forms interface for adjusting SalesOrderAllocation objects
v14 -> 2021-10-05
- Stock adjustment actions API is improved, using native DRF serializer support
- However adjustment actions now only support 'pk' as a lookup field
v13 -> 2021-10-05
- Adds API endpoint to allocate stock items against a BuildOrder
- Updates StockItem API with improved filtering against BomItem data
v12 -> 2021-09-07
- Adds API endpoint to receive stock items against a PurchaseOrder
v11 -> 2021-08-26
- Adds "units" field to PartBriefSerializer
- This allows units to be introspected from the "part_detail" field in the StockItem serializer
v10 -> 2021-08-23
- Adds "purchase_price_currency" to StockItem serializer
- Adds "purchase_price_string" to StockItem serializer
- Purchase price is now writable for StockItem serializer
v9 -> 2021-08-09
- Adds "price_string" to part pricing serializers
v8 -> 2021-07-19
- Refactors the API interface for SupplierPart and ManufacturerPart models
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
v7 -> 2021-07-03
- Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716
- API OPTIONS endpoints provide comprehensive field metedata
- Multiple new API endpoints added for database models
v6 -> 2021-06-23
- Part and Company images can now be directly uploaded via the REST API
v5 -> 2021-06-21
- Adds API interface for manufacturer part parameters
v4 -> 2021-06-01
- BOM items can now accept "variant stock" to be assigned against them
- Many slight API tweaks were needed to get this to work properly!
v3 -> 2021-05-22:
- The updated StockItem "history tracking" now uses a different interface
"""
def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """

View File

@ -3,7 +3,6 @@
{% load i18n %}
{% load inventree_extras %}
{% load status_codes %}
{% load markdownify %}
{% block sidebar %}
{% include "build/sidebar.html" %}
@ -309,24 +308,16 @@
<div class='panel panel-hidden' id='panel-notes'>
<div class='panel-heading'>
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Build Notes" %}</h4>
</div>
<div class='col-sm-6'>
<div class='btn-group float-right'>
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn btn-small btn-outline-secondary'>
<span class='fas fa-edit'>
</span>
</button>
</div>
<div class='d-flex flex-wrap'>
<h4>{% trans "Build Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
{% if build.notes %}
{{ build.notes | markdownify }}
{% endif %}
<textarea id='build-notes'></textarea>
</div>
</div>
@ -392,17 +383,18 @@ onPanelLoad('attachments', function() {
});
onPanelLoad('notes', function() {
$('#edit-notes').click(function() {
constructForm('{% url "api-build-detail" build.pk %}', {
fields: {
notes: {
multiline: true,
}
},
title: '{% trans "Edit Notes" %}',
reload: true,
});
});
setupNotesField(
'build-notes',
'{% url "api-build-detail" build.pk %}',
{
{% if roles.build.change %}
editable: true,
{% else %}
editable: false,
{% endif %}
}
);
});
function reloadTable() {

View File

@ -1,7 +1,6 @@
{% extends "company/company_base.html" %}
{% load static %}
{% load i18n %}
{% load markdownify %}
{% block sidebar %}
{% include 'company/sidebar.html' %}
@ -181,24 +180,16 @@
<div class='panel panel-hidden' id='panel-company-notes'>
<div class='panel-heading'>
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Company Notes" %}</h4>
</div>
<div class='col-sm-6'>
<div class='btn-group float-right'>
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn btn-small btn-outline-secondary'>
<span class='fas fa-edit'>
</span>
</button>
</div>
<div class='d-flex flex-wrap'>
<h4>{% trans "Company Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
{% if company.notes %}
{{ company.notes | markdownify }}
{% endif %}
<textarea id='company-notes'></textarea>
</div>
</div>
@ -207,16 +198,15 @@
{% block js_ready %}
{{ block.super }}
$('#edit-notes').click(function() {
constructForm('{% url "api-company-detail" company.pk %}', {
fields: {
notes: {
multiline: true,
}
},
title: '{% trans "Edit Notes" %}',
reload: true,
});
onPanelLoad('company-notes', function() {
setupNotesField(
'company-notes',
'{% url "api-company-detail" company.pk %}',
{
editable: true,
}
)
});
loadStockTable($("#assigned-stock-table"), {
@ -230,18 +220,37 @@
filterTarget: '#filter-list-customerstock',
});
{% if company.is_customer %}
loadSalesOrderTable("#sales-order-table", {
url: "{% url 'api-so-list' %}",
params: {
customer: {{ company.id }},
}
onPanelLoad('company-stock', function() {
loadStockTable($('#stock-table'), {
url: "{% url 'api-stock-list' %}",
params: {
company: {{ company.id }},
part_detail: true,
supplier_part_detail: true,
location_detail: true,
},
buttons: [
'#stock-options',
],
filterKey: "companystock",
});
});
$("#new-sales-order").click(function() {
{% if company.is_customer %}
onPanelLoad('sales-orders', function() {
loadSalesOrderTable("#sales-order-table", {
url: "{% url 'api-so-list' %}",
params: {
customer: {{ company.id }},
}
});
createSalesOrder({
customer: {{ company.pk }},
$("#new-sales-order").click(function() {
createSalesOrder({
customer: {{ company.pk }},
});
});
});
{% endif %}
@ -270,20 +279,6 @@
{% endif %}
loadStockTable($('#stock-table'), {
url: "{% url 'api-stock-list' %}",
params: {
company: {{ company.id }},
part_detail: true,
supplier_part_detail: true,
location_detail: true,
},
buttons: [
'#stock-options',
],
filterKey: "companystock",
});
{% if company.is_manufacturer %}
function reloadManufacturerPartTable() {

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,10 +11,14 @@
{% block page_content %}
{% trans "Upload File for Purchase Order" as header_text %}
{% order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change as upload_go_ahead %}
{% trans "Order is already processed. Files cannot be uploaded." as error_text %}
{% "panel-upload-file" as panel_id %}
{% include "patterns/wizard/upload.html" with header_text=header_text upload_go_ahead=upload_go_ahead error_text=error_text panel_id=panel_id %}
{% with "panel-upload-file" as panel_id %}
{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
{% include "patterns/wizard/upload.html" with header_text=header_text upload_go_ahead=True error_text=error_text panel_id=panel_id %}
{% else %}
{% include "patterns/wizard/upload.html" with header_text=header_text upload_go_ahead=False error_text=error_text panel_id=panel_id %}
{% endif %}
{% endwith %}
{% endblock %}
{% block js_ready %}

View File

@ -4,7 +4,6 @@
{% load status_codes %}
{% load i18n %}
{% load static %}
{% load markdownify %}
{% block sidebar %}
{% include 'order/po_sidebar.html' %}
@ -71,24 +70,16 @@
<div class='panel panel-hidden' id='panel-order-notes'>
<div class='panel-heading'>
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Order Notes" %}</h4>
</div>
<div class='col-sm-6'>
<div class='btn-group float-right'>
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn btn-outline-secondary'>
<span class='fas fa-edit'>
</span>
</button>
</div>
<div class='d-flex flex-wrap'>
<h4>{% trans "Order Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
{% if order.notes %}
{{ order.notes | markdownify }}
{% endif %}
<textarea id='order-notes'></textarea>
</div>
</div>
@ -98,16 +89,18 @@
{{ block.super }}
$('#edit-notes').click(function() {
constructForm('{% url "api-po-detail" order.pk %}', {
fields: {
notes: {
multiline: true,
}
},
title: '{% trans "Edit Notes" %}',
reload: true,
});
onPanelLoad('order-notes', function() {
setupNotesField(
'order-notes',
'{% url "api-po-detail" order.pk %}',
{
{% if roles.purchase_order.change %}
editable: true,
{% else %}
editable: false,
{% endif %}
}
);
});
enableDragAndDrop(

View File

@ -4,7 +4,6 @@
{% load status_codes %}
{% load i18n %}
{% load static %}
{% load markdownify %}
{% block sidebar %}
{% include "order/so_sidebar.html" %}
@ -118,24 +117,16 @@
<div class='panel panel-hidden' id='panel-order-notes'>
<div class='panel-heading'>
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Order Notes" %}</h4>
</div>
<div class='col-sm-6'>
<div class='btn-group float-right'>
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn outline-secondary'>
<span class='fas fa-edit'>
</span>
</button>
</div>
<div class='d-flex flex-wrap'>
<h4>{% trans "Order Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
{% if order.notes %}
{{ order.notes | markdownify }}
{% endif %}
<textarea id='order-notes'></textarea>
</div>
</div>
@ -176,16 +167,18 @@
});
});
$('#edit-notes').click(function() {
constructForm('{% url "api-so-detail" order.pk %}', {
fields: {
notes: {
multiline: true,
}
},
title: '{% trans "Edit Notes" %}',
reload: true,
});
onPanelLoad('order-notes', function() {
setupNotesField(
'order-notes',
'{% url "api-so-detail" order.pk %}',
{
{% if roles.purchase_order.change %}
editable: true,
{% else %}
editable: false,
{% endif %}
}
);
});
enableDragAndDrop(

View File

@ -1602,9 +1602,10 @@ class BomList(generics.ListCreateAPIView):
def get_queryset(self, *args, **kwargs):
queryset = BomItem.objects.all()
queryset = super().get_queryset(*args, **kwargs)
queryset = self.get_serializer_class().setup_eager_loading(queryset)
queryset = self.get_serializer_class().annotate_queryset(queryset)
return queryset
@ -1818,6 +1819,15 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = BomItem.objects.all()
serializer_class = part_serializers.BomItemSerializer
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = self.get_serializer_class().setup_eager_loading(queryset)
queryset = self.get_serializer_class().annotate_queryset(queryset)
return queryset
class BomItemValidate(generics.UpdateAPIView):
""" API endpoint for validating a BomItem """

View File

@ -1313,19 +1313,31 @@ class Part(MPTTModel):
return quantity
def build_order_allocations(self):
def build_order_allocations(self, **kwargs):
"""
Return all 'BuildItem' objects which allocate this part to Build objects
"""
return BuildModels.BuildItem.objects.filter(stock_item__part__id=self.id)
include_variants = kwargs.get('include_variants', True)
def build_order_allocation_count(self):
queryset = BuildModels.BuildItem.objects.all()
if include_variants:
variants = self.get_descendants(include_self=True)
queryset = queryset.filter(
stock_item__part__in=variants,
)
else:
queryset = queryset.filter(stock_item__part=self)
return queryset
def build_order_allocation_count(self, **kwargs):
"""
Return the total amount of this part allocated to build orders
"""
query = self.build_order_allocations().aggregate(
query = self.build_order_allocations(**kwargs).aggregate(
total=Coalesce(
Sum(
'quantity',
@ -1343,7 +1355,19 @@ class Part(MPTTModel):
Return all sales-order-allocation objects which allocate this part to a SalesOrder
"""
queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
include_variants = kwargs.get('include_variants', True)
queryset = OrderModels.SalesOrderAllocation.objects.all()
if include_variants:
# Include allocations for all variants
variants = self.get_descendants(include_self=True)
queryset = queryset.filter(
item__part__in=variants,
)
else:
# Only look at this part
queryset = queryset.filter(item__part=self)
# Default behaviour is to only return *pending* allocations
pending = kwargs.get('pending', True)
@ -1381,7 +1405,7 @@ class Part(MPTTModel):
return query['total']
def allocation_count(self):
def allocation_count(self, **kwargs):
"""
Return the total quantity of stock allocated for this part,
against both build orders and sales orders.
@ -1389,8 +1413,8 @@ class Part(MPTTModel):
return sum(
[
self.build_order_allocation_count(),
self.sales_order_allocation_count(),
self.build_order_allocation_count(**kwargs),
self.sales_order_allocation_count(**kwargs),
],
)
@ -2882,23 +2906,6 @@ class BomItem(models.Model, DataImportMixin):
child=self.sub_part.full_name,
n=decimal2string(self.quantity))
def available_stock(self):
"""
Return the available stock items for the referenced sub_part
"""
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)
)
return query['available']
def get_overage_quantity(self, quantity):
""" Calculate overage quantity
"""

View File

@ -577,6 +577,10 @@ class BomItemSerializer(InvenTreeModelSerializer):
purchase_price_range = serializers.SerializerMethodField()
# Annotated fields
available_stock = serializers.FloatField(read_only=True)
available_substitute_stock = serializers.FloatField(read_only=True)
def __init__(self, *args, **kwargs):
# part_detail and sub_part_detail serializers are only included if requested.
# This saves a bunch of database requests
@ -609,10 +613,110 @@ class BomItemSerializer(InvenTreeModelSerializer):
queryset = queryset.prefetch_related('sub_part')
queryset = queryset.prefetch_related('sub_part__category')
queryset = queryset.prefetch_related('sub_part__stock_items')
queryset = queryset.prefetch_related(
'sub_part__stock_items',
'sub_part__stock_items__allocations',
'sub_part__stock_items__sales_order_allocations',
)
queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks')
return queryset
@staticmethod
def annotate_queryset(queryset):
"""
Annotate the BomItem queryset with extra information:
Annotations:
available_stock: The amount of stock available for the sub_part Part object
"""
"""
Construct an "available stock" quantity:
available_stock = total_stock - build_order_allocations - sales_order_allocations
"""
build_order_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES)
sales_order_filter = Q(
line__order__status__in=SalesOrderStatus.OPEN,
shipment__shipment_date=None,
)
# Calculate "total stock" for the referenced sub_part
# Calculate the "build_order_allocations" for the sub_part
# Note that these fields are only aliased, not annotated
queryset = queryset.alias(
total_stock=Coalesce(
SubquerySum(
'sub_part__stock_items__quantity',
filter=StockItem.IN_STOCK_FILTER
),
Decimal(0),
output_field=models.DecimalField(),
),
allocated_to_sales_orders=Coalesce(
SubquerySum(
'sub_part__stock_items__sales_order_allocations__quantity',
filter=sales_order_filter,
),
Decimal(0),
output_field=models.DecimalField(),
),
allocated_to_build_orders=Coalesce(
SubquerySum(
'sub_part__stock_items__allocations__quantity',
filter=build_order_filter,
),
Decimal(0),
output_field=models.DecimalField(),
),
)
# Calculate 'available_stock' based on previously annotated fields
queryset = queryset.annotate(
available_stock=ExpressionWrapper(
F('total_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'),
output_field=models.DecimalField(),
)
)
# Extract similar information for any 'substitute' parts
queryset = queryset.alias(
substitute_stock=Coalesce(
SubquerySum(
'substitutes__part__stock_items__quantity',
filter=StockItem.IN_STOCK_FILTER,
),
Decimal(0),
output_field=models.DecimalField(),
),
substitute_build_allocations=Coalesce(
SubquerySum(
'substitutes__part__stock_items__allocations__quantity',
filter=build_order_filter,
),
Decimal(0),
output_field=models.DecimalField(),
),
substitute_sales_allocations=Coalesce(
SubquerySum(
'substitutes__part__stock_items__sales_order_allocations__quantity',
filter=sales_order_filter,
),
Decimal(0),
output_field=models.DecimalField(),
),
)
# Calculate 'available_variant_stock' field
queryset = queryset.annotate(
available_substitute_stock=ExpressionWrapper(
F('substitute_stock') - F('substitute_build_allocations') - F('substitute_sales_allocations'),
output_field=models.DecimalField(),
)
)
return queryset
def get_purchase_price_range(self, obj):
""" Return purchase price range """
@ -682,6 +786,10 @@ class BomItemSerializer(InvenTreeModelSerializer):
'substitutes',
'price_range',
'validated',
# Annotated fields describing available quantity
'available_stock',
'available_substitute_stock',
]

View File

@ -1,10 +0,0 @@
{% load i18n %}
<div class="markdownx row">
<div class="markdown col-md-6">
{% include 'django/forms/widgets/textarea.html' %}
</div>
<div class="markdown col-md-6">
<div class="markdownx-preview"></div>
</div>
</div>

View File

@ -187,6 +187,15 @@
</div>
</div>
<div class='panel panel-hidden' id='panel-stock'>
<div class='panel-heading'>
<h4>{% trans "Stock Items" %}</h4>
</div>
<div class='panel-content'>
{% include "stock_table.html" %}
</div>
</div>
<div class='panel panel-hidden' id='panel-parameters'>
<div class='panel-heading'>
<h4>{% trans "Part Parameters" %}</h4>
@ -223,6 +232,21 @@
{{ block.super }}
{% if category %}
onPanelLoad('stock', function() {
loadStockTable(
$('#stock-table'),
{
params: {
category: {{ category.pk }},
part_detail: true,
location_detail: true,
supplier_part_detail: true,
}
}
);
});
onPanelLoad('parameters', function() {
loadParametricPartTable(
"#parametric-part-table",

View File

@ -14,6 +14,8 @@
{% include "sidebar_link.html" with url=url text=text icon="fa-file-upload" %}
{% endif %}
{% if category %}
{% trans "Stock Items" as text %}
{% include "sidebar_item.html" with label='stock' text=text icon='fa-boxes' %}
{% trans "Parameters" as text %}
{% include "sidebar_item.html" with label="parameters" text=text icon="fa-tasks" %}
{% endif %}

View File

@ -3,7 +3,6 @@
{% load i18n %}
{% load inventree_extras %}
{% load crispy_forms_tags %}
{% load markdownify %}
{% block sidebar %}
{% include 'part/part_sidebar.html' %}
@ -134,24 +133,16 @@
<div class='panel panel-hidden' id='panel-part-notes'>
<div class='panel-heading'>
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Notes" %}</h4>
</div>
<div class='col-sm-6'>
<div class='btn-group float-right'>
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn btn-outline-secondary'>
<span class='fas fa-edit'>
</span>
</button>
</div>
<div class='d-flex flex-wrap'>
<h4>{% trans "Part Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
{% if part.notes %}
{{ part.notes | markdownify }}
{% endif %}
<textarea id='part-notes'></textarea>
</div>
</div>
@ -419,6 +410,18 @@
{% block js_ready %}
{{ block.super }}
// Load the "notes" tab
onPanelLoad('part-notes', function() {
setupNotesField(
'part-notes',
'{% url "api-part-detail" part.pk %}',
{
editable: {% if roles.part.change %}true{% else %}false{% endif %},
}
);
});
// Load the "scheduling" tab
onPanelLoad('scheduling', function() {
loadPartSchedulingChart('part-schedule-chart', {{ part.pk }});
@ -832,36 +835,6 @@
});
});
$('#edit-notes').click(function() {
constructForm('{% url "api-part-detail" part.pk %}', {
fields: {
notes: {
multiline: true,
}
},
title: '{% trans "Edit Part Notes" %}',
reload: true,
});
});
$(".slidey").change(function() {
var field = $(this).attr('fieldname');
var checked = $(this).prop('checked');
var data = {};
data[field] = checked;
// Update the particular field
inventreePut("{% url 'api-part-detail' part.id %}",
data,
{
method: 'PATCH',
reloadOnSuccess: true,
},
);
});
onPanelLoad("part-parameters", function() {
loadPartParameterTable(
'#parameter-table',

View File

@ -11,9 +11,8 @@
{% block content %}
{% trans "Import Parts from File" as header_text %}
{% roles.part.change as upload_go_ahead %}
{% trans "Unsuffitient privileges." as error_text %}
{% include "patterns/wizard/upload.html" with header_text=header_text upload_go_ahead=upload_go_ahead error_text=error_text %}
{% include "patterns/wizard/upload.html" with header_text=header_text upload_go_ahead=roles.part.change error_text=error_text %}
{% endblock %}
{% block js_ready %}

View File

@ -252,7 +252,6 @@
</tr>
{% endif %}
{% endif %}
{% if not part.is_template %}
{% if part.assembly %}
<tr>
<td><span class='fas fa-tools'></span></td>
@ -266,7 +265,6 @@
<td>{% decimal quantity_being_built %}</td>
</tr>
{% endif %}
{% endif %}
{% endif %}
</table>
{% endblock details_right %}

View File

@ -9,7 +9,7 @@ from rest_framework import status
from rest_framework.test import APIClient
from InvenTree.api_tester import InvenTreeAPITestCase
from InvenTree.status_codes import BuildStatus, StockStatus
from InvenTree.status_codes import BuildStatus, StockStatus, PurchaseOrderStatus
from part.models import Part, PartCategory
from part.models import BomItem, BomItemSubstitute
@ -578,7 +578,12 @@ class PartDetailTests(InvenTreeAPITestCase):
'part',
'location',
'bom',
'company',
'test_templates',
'manufacturer_part',
'supplier_part',
'order',
'stock',
]
roles = [
@ -805,6 +810,38 @@ class PartDetailTests(InvenTreeAPITestCase):
# And now check that the image has been set
p = Part.objects.get(pk=pk)
def test_details(self):
"""
Test that the required details are available
"""
p = Part.objects.get(pk=1)
url = reverse('api-part-detail', kwargs={'pk': 1})
data = self.get(url, expected_code=200).data
# How many parts are 'on order' for this part?
lines = order.models.PurchaseOrderLineItem.objects.filter(
part__part__pk=1,
order__status__in=PurchaseOrderStatus.OPEN,
)
on_order = 0
# Calculate the "on_order" quantity by hand,
# to check it matches the API value
for line in lines:
on_order += line.quantity
on_order -= line.received
self.assertEqual(on_order, data['ordering'])
self.assertEqual(on_order, p.on_order)
# Some other checks
self.assertEqual(data['in_stock'], 9000)
self.assertEqual(data['unallocated_stock'], 9000)
class PartAPIAggregationTest(InvenTreeAPITestCase):
"""
@ -1123,6 +1160,12 @@ class BomItemTest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['pk'], bom_item.pk)
# Each item in response should contain expected keys
for el in response.data:
for key in ['available_stock', 'available_substitute_stock']:
self.assertTrue(key in el)
def test_get_bom_detail(self):
"""
Get the detail view for a single BomItem object
@ -1132,6 +1175,26 @@ class BomItemTest(InvenTreeAPITestCase):
response = self.get(url, expected_code=200)
expected_values = [
'allow_variants',
'inherited',
'note',
'optional',
'overage',
'pk',
'part',
'quantity',
'reference',
'sub_part',
'substitutes',
'validated',
'available_stock',
'available_substitute_stock',
]
for key in expected_values:
self.assertTrue(key in response.data)
self.assertEqual(int(float(response.data['quantity'])), 25)
# Increase the quantity
@ -1319,6 +1382,21 @@ class BomItemTest(InvenTreeAPITestCase):
response = self.get(url, expected_code=200)
self.assertEqual(len(response.data), 5)
# The BomItem detail endpoint should now also reflect the substitute data
data = self.get(
reverse('api-bom-item-detail', kwargs={'pk': bom_item.pk}),
expected_code=200
).data
# 5 substitute parts
self.assertEqual(len(data['substitutes']), 5)
# 5 x 1,000 stock quantity
self.assertEqual(data['available_substitute_stock'], 5000)
# 9,000 stock directly available
self.assertEqual(data['available_stock'], 9000)
def test_bom_item_uses(self):
"""
Tests for the 'uses' field

View File

@ -4,7 +4,6 @@
{% load report %}
{% load barcode %}
{% load inventree_extras %}
{% load markdownify %}
{% block page_margin %}
margin: 2cm;

View File

@ -402,11 +402,51 @@ class StockFilter(rest_filters.FilterSet):
serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
def filter_serialized(self, queryset, name, value):
"""
Filter by whether the StockItem has a serial number (or not)
"""
q = Q(serial=None) | Q(serial='')
if str2bool(value):
queryset = queryset.exclude(serial=None)
queryset = queryset.exclude(q)
else:
queryset = queryset.filter(serial=None)
queryset = queryset.filter(q)
return queryset
has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch')
def filter_has_batch(self, queryset, name, value):
"""
Filter by whether the StockItem has a batch code (or not)
"""
q = Q(batch=None) | Q(batch='')
if str2bool(value):
queryset = queryset.exclude(q)
else:
queryset = queryset.filter(q)
return queryset
tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked')
def filter_tracked(self, queryset, name, value):
"""
Filter by whether this stock item is *tracked*, meaning either:
- It has a serial number
- It has a batch code
"""
q_batch = Q(batch=None) | Q(batch='')
q_serial = Q(serial=None) | Q(serial='')
if str2bool(value):
queryset = queryset.exclude(q_batch & q_serial)
else:
queryset = queryset.filter(q_batch & q_serial)
return queryset
@ -1105,7 +1145,6 @@ class StockItemTestResultList(generics.ListCreateAPIView):
]
filter_fields = [
'stock_item',
'test',
'user',
'result',
@ -1114,6 +1153,38 @@ class StockItemTestResultList(generics.ListCreateAPIView):
ordering = 'date'
def filter_queryset(self, queryset):
params = self.request.query_params
queryset = super().filter_queryset(queryset)
# Filter by stock item
item = params.get('stock_item', None)
if item is not None:
try:
item = StockItem.objects.get(pk=item)
items = [item]
# Do we wish to also include test results for 'installed' items?
include_installed = str2bool(params.get('include_installed', False))
if include_installed:
# Include items which are installed "underneath" this item
# Note that this function is recursive!
installed_items = item.get_installed_items(cascade=True)
items += [it for it in installed_items]
queryset = queryset.filter(stock_item__in=items)
except (ValueError, StockItem.DoesNotExist):
pass
return queryset
def get_serializer(self, *args, **kwargs):
try:
kwargs['user_detail'] = str2bool(self.request.query_params.get('user_detail', False))
@ -1189,6 +1260,15 @@ class StockTrackingList(generics.ListAPIView):
if not deltas:
deltas = {}
# Add part detail
if 'part' in deltas:
try:
part = Part.objects.get(pk=deltas['part'])
serializer = PartBriefSerializer(part)
deltas['part_detail'] = serializer.data
except:
pass
# Add location detail
if 'location' in deltas:
try:

View File

@ -453,6 +453,12 @@ class StockItem(MPTTModel):
super().clean()
if self.serial is not None and type(self.serial) is str:
self.serial = self.serial.strip()
if self.batch is not None and type(self.batch) is str:
self.batch = self.batch.strip()
try:
if self.part.trackable:
# Trackable parts must have integer values for quantity field!
@ -718,6 +724,33 @@ class StockItem(MPTTModel):
help_text=_('Select Owner'),
related_name='stock_items')
@transaction.atomic
def convert_to_variant(self, variant, user, notes=None):
"""
Convert this StockItem instance to a "variant",
i.e. change the "part" reference field
"""
if not variant:
# Ignore null values
return
if variant == self.part:
# Variant is the same as the current part
return
self.part = variant
self.save()
self.add_tracking_entry(
StockHistoryCode.CONVERTED_TO_VARIANT,
user,
deltas={
'part': variant.pk,
},
notes=_('Converted to part') + ': ' + variant.full_name,
)
def get_item_owner(self):
"""
Return the closest "owner" for this StockItem.

View File

@ -4,7 +4,6 @@
{% load inventree_extras %}
{% load i18n %}
{% load l10n %}
{% load markdownify %}
{% block sidebar %}
{% include "stock/stock_sidebar.html" %}
@ -27,11 +26,12 @@
</div>
</div>
<div class='panel-content'>
<div id='table-toolbar'>
<div id='tracking-table-toolbar'>
<div class='btn-group'>
{% include "filter_list.html" with id="stocktracking" %}
</div>
</div>
<table class='table table-condensed table-striped' id='track-table' data-toolbar='#table-toolbar'>
<table class='table table-condensed table-striped' id='track-table' data-toolbar='#tracking-table-toolbar'>
</table>
</div>
</div>
@ -133,24 +133,16 @@
<div class='panel panel-hidden' id='panel-notes'>
<div class='panel-heading'>
<div class='row'>
<div class='col-sm-6'>
<h4>{% trans "Stock Item Notes" %}</h4>
</div>
<div class='col-sm-6'>
<div class='btn-group float-right'>
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn btn-small btn-outline-secondary'>
<span class='fas fa-edit'>
</span>
</button>
</div>
<div class='d-flex flex-wrap'>
<h4>{% trans "Stock Item Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
{% if item.notes %}
{{ item.notes | markdownify }}
{% endif %}
<textarea id='stock-notes'></textarea>
</div>
</div>
@ -235,18 +227,21 @@
reload: true,
}
);
});
$('#edit-notes').click(function() {
constructForm('{% url "api-stock-detail" item.pk %}', {
fields: {
notes: {
multiline: true,
}
},
title: '{% trans "Edit Notes" %}',
reload: true,
});
});
onPanelLoad('notes', function() {
setupNotesField(
'stock-notes',
'{% url "api-stock-detail" item.pk %}',
{
{% if roles.stock.change and user_owns_item %}
editable: true,
{% else %}
editable: false,
{% endif %}
}
);
});
enableDragAndDrop(
@ -348,7 +343,6 @@
);
});
loadStockTrackingTable($("#track-table"), {
params: {
ordering: '-date',

View File

@ -505,7 +505,12 @@ $("#barcode-unlink").click(function() {
});
$("#barcode-scan-into-location").click(function() {
scanItemsIntoLocation([{{ item.id }}]);
inventreeGet('{% url "api-stock-detail" item.pk %}', {}, {
success: function(item) {
scanItemsIntoLocation([item]);
}
});
});
function itemAdjust(action) {

View File

@ -210,6 +210,46 @@ class StockItemListTest(StockAPITestCase):
for item in response:
self.assertIsNone(item['serial'])
def test_filter_by_has_batch(self):
"""
Test the 'has_batch' filter, which tests if the stock item has been assigned a batch code
"""
with_batch = self.get_stock(has_batch=1)
without_batch = self.get_stock(has_batch=0)
n_stock_items = StockItem.objects.all().count()
# Total sum should equal the total count of stock items
self.assertEqual(n_stock_items, len(with_batch) + len(without_batch))
for item in with_batch:
self.assertFalse(item['batch'] in [None, ''])
for item in without_batch:
self.assertTrue(item['batch'] in [None, ''])
def test_filter_by_tracked(self):
"""
Test the 'tracked' filter.
This checks if the stock item has either a batch code *or* a serial number
"""
tracked = self.get_stock(tracked=True)
untracked = self.get_stock(tracked=False)
n_stock_items = StockItem.objects.all().count()
self.assertEqual(n_stock_items, len(tracked) + len(untracked))
blank = [None, '']
for item in tracked:
self.assertTrue(item['batch'] not in blank or item['serial'] not in blank)
for item in untracked:
self.assertTrue(item['batch'] in blank and item['serial'] in blank)
def test_filter_by_expired(self):
"""
Filter StockItem by expiry status

View File

@ -644,6 +644,16 @@ class StockItemConvert(AjaxUpdateView):
return form
def save(self, obj, form):
stock_item = self.get_object()
variant = form.cleaned_data.get('part', None)
stock_item.convert_to_variant(variant, user=self.request.user)
return stock_item
class StockLocationCreate(AjaxCreateView):
"""

View File

@ -24,6 +24,7 @@
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_URL" icon="fa-link" %}
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_NAVIGATION" icon="fa-sitemap" %}
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %}
{% include "InvenTree/settings/setting.html" with key="PLUGIN_ON_STARTUP" %}
</tbody>
</table>
</div>

View File

@ -89,7 +89,7 @@ $('table').find('.boolean-setting').change(function() {
},
{
method: 'PATCH',
onSuccess: function(data) {
success: function(data) {
},
error: function(xhr) {
showApiError(xhr, url);

View File

@ -48,6 +48,7 @@
<link rel="stylesheet" href="{% static 'select2/css/select2-bootstrap-5-theme.css' %}">
<link rel="stylesheet" href="{% static 'fullcalendar/main.css' %}">
<link rel="stylesheet" href="{% static 'script/jquery-ui/jquery-ui.min.css' %}">
<link rel="stylesheet" href="{% static 'easymde/easymde.min.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
@ -160,6 +161,7 @@
<script type='text/javascript' src="{% static 'script/chart.js' %}"></script>
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
<script type='text/javascript' src="{% static 'script/chartjs-adapter-moment.js' %}"></script>
<script type='text/javascript' src="{% static 'easymde/easymde.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>

View File

@ -179,6 +179,11 @@ function showApiError(xhr, url) {
var title = null;
var message = null;
if (xhr.statusText == 'abort') {
// Don't show errors for requests which were intentionally aborted
return;
}
switch (xhr.status || 0) {
// No response
case 0:

View File

@ -359,14 +359,13 @@ function unlinkBarcode(stockitem) {
/*
* Display dialog to check multiple stock items in to a stock location.
*/
function barcodeCheckIn(location_id) {
function barcodeCheckIn(location_id, options={}) {
var modal = '#modal-form';
// List of items we are going to checkin
var items = [];
function reloadTable() {
modalEnable(modal, false);
@ -389,10 +388,17 @@ function barcodeCheckIn(location_id) {
<tbody>`;
items.forEach(function(item) {
var location_info = `${item.location}`;
if (item.location_detail) {
location_info = `${item.location_detail.name}`;
}
html += `
<tr pk='${item.pk}'>
<td>${imageHoverIcon(item.part_detail.thumbnail)} ${item.part_detail.name}</td>
<td>${item.location_detail.name}</td>
<td>${location_info}</td>
<td>${item.quantity}</td>
<td>${makeIconButton('fa-times-circle icon-red', 'button-item-remove', item.pk, '{% trans "Remove stock item" %}')}</td>
</tr>`;
@ -469,6 +475,12 @@ function barcodeCheckIn(location_id) {
data.items = entries;
// Prevent submission without any entries
if (entries.length == 0) {
showBarcodeMessage(modal, '{% trans "No barcode provided" %}', 'warning');
return;
}
inventreePut(
'{% url "api-stock-transfer" %}',
data,
@ -477,15 +489,11 @@ function barcodeCheckIn(location_id) {
success: function(response, status) {
// Hide the modal
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
addCachedAlert(response.success);
location.reload();
if (options.success) {
options.success(response);
} else {
showMessage('{% trans "Error transferring stock" %}', {
style: 'danger',
icon: 'fas fa-times-circle',
});
location.reload();
}
}
}
@ -533,7 +541,7 @@ function barcodeCheckIn(location_id) {
/*
* Display dialog to check a single stock item into a stock location
*/
function scanItemsIntoLocation(item_id_list, options={}) {
function scanItemsIntoLocation(item_list, options={}) {
var modal = options.modal || '#modal-form';
@ -583,9 +591,10 @@ function scanItemsIntoLocation(item_id_list, options={}) {
var items = [];
item_id_list.forEach(function(pk) {
item_list.forEach(function(item) {
items.push({
pk: pk,
pk: item.pk || item.id,
quantity: item.quantity,
});
});
@ -605,13 +614,10 @@ function scanItemsIntoLocation(item_id_list, options={}) {
// First hide the modal
$(modal).modal('hide');
if (status == 'success' && 'success' in response) {
addCachedAlert(response.success);
location.reload();
if (options.success) {
options.success(response);
} else {
showMessage('{% trans "Error transferring stock" %}', {
style: 'danger',
});
location.reload();
}
}
}

View File

@ -798,17 +798,25 @@ function loadBomTable(table, options={}) {
});
cols.push({
field: 'sub_part_detail.stock',
field: 'available_stock',
title: '{% trans "Available" %}',
searchable: false,
sortable: true,
formatter: function(value, row) {
var url = `/part/${row.sub_part_detail.pk}/?display=part-stock`;
var text = value;
if (value == null || value <= 0) {
text = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
// Calculate total "available" (unallocated) quantity
var total = row.available_stock + row.available_substitute_stock;
var text = `${total}`;
if (total <= 0) {
text = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock Available" %}</span>`;
} else {
if (row.available_substitute_stock > 0) {
text += `<span title='{% trans "Includes substitute stock" %}' class='fas fa-info-circle float-right icon-blue'></span>`;
}
}
return renderLink(text, url);
@ -902,8 +910,10 @@ function loadBomTable(table, options={}) {
formatter: function(value, row) {
var can_build = 0;
var available = row.available_stock + row.available_substitute_stock;
if (row.quantity > 0) {
can_build = row.sub_part_detail.stock / row.quantity;
can_build = available / row.quantity;
}
return +can_build.toFixed(2);
@ -914,11 +924,11 @@ function loadBomTable(table, options={}) {
var cb_b = 0;
if (rowA.quantity > 0) {
cb_a = rowA.sub_part_detail.stock / rowA.quantity;
cb_a = (rowA.available_stock + rowA.available_substitute_stock) / rowA.quantity;
}
if (rowB.quantity > 0) {
cb_b = rowB.sub_part_detail.stock / rowB.quantity;
cb_b = (rowB.available_stock + rowB.available_substitute_stock) / rowB.quantity;
}
return (cb_a > cb_b) ? 1 : -1;

View File

@ -1421,9 +1421,24 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
sortable: true,
},
{
field: 'sub_part_detail.stock',
field: 'available_stock',
title: '{% trans "Available" %}',
sortable: true,
formatter: function(value, row) {
var total = row.available_stock + row.available_substitute_stock;
var text = `${total}`;
if (total <= 0) {
text = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock Available" %}</span>`;
} else {
if (row.available_substitute_stock > 0) {
text += `<span title='{% trans "Includes substitute stock" %}' class='fas fa-info-circle float-right icon-blue'></span>`;
}
}
return text;
}
},
{
field: 'allocated',

View File

@ -10,6 +10,7 @@
makeProgressBar,
renderLink,
select2Thumbnail,
setupNotesField,
thumbnailImage
yesNoLabel,
*/
@ -221,3 +222,93 @@ function renderLink(text, url, options={}) {
return `<a href="${url}">${text}</a>`;
}
function setupNotesField(element, url, options={}) {
var editable = options.editable || false;
// Read initial notes value from the URL
var initial = null;
inventreeGet(url, {}, {
async: false,
success: function(response) {
initial = response[options.notes_field || 'notes'];
},
});
var toolbar_icons = [
'preview', '|',
];
if (editable) {
// Heading icons
toolbar_icons.push('heading-1', 'heading-2', 'heading-3', '|');
// Font style
toolbar_icons.push('bold', 'italic', 'strikethrough', '|');
// Text formatting
toolbar_icons.push('unordered-list', 'ordered-list', 'code', 'quote', '|');
// Elements
toolbar_icons.push('table', 'link', 'image');
}
// Markdown syntax guide
toolbar_icons.push('|', 'guide');
const mde = new EasyMDE({
element: document.getElementById(element),
initialValue: initial,
toolbar: toolbar_icons,
shortcuts: [],
});
// Hide the toolbar
$(`#${element}`).next('.EasyMDEContainer').find('.editor-toolbar').hide();
if (!editable) {
// Set readonly
mde.codemirror.setOption('readOnly', true);
// Hide the "edit" and "save" buttons
$('#edit-notes').hide();
$('#save-notes').hide();
} else {
mde.togglePreview();
// Add callback for "edit" button
$('#edit-notes').click(function() {
$('#edit-notes').hide();
$('#save-notes').show();
// Show the toolbar
$(`#${element}`).next('.EasyMDEContainer').find('.editor-toolbar').show();
mde.togglePreview();
});
// Add callback for "save" button
$('#save-notes').click(function() {
var data = {};
data[options.notes_field || 'notes'] = mde.value();
inventreePut(url, data, {
method: 'PATCH',
success: function(response) {
showMessage('{% trans "Notes updated" %}', {style: 'success'});
},
error: function(xhr) {
showApiError(xhr, url);
}
});
});
}
}

View File

@ -34,8 +34,8 @@
// Should the ID be rendered for this string
function renderId(title, pk, parameters={}) {
// Default = true
var render = true;
// Default = do not display
var render = false;
if ('render_pk' in parameters) {
render = parameters['render_pk'];
@ -99,14 +99,22 @@ function renderStockItem(name, data, parameters={}, options={}) {
var stock_detail = '';
if (data.serial && data.quantity == 1) {
stock_detail = `{% trans "Serial Number" %}: ${data.serial}`;
} else if (data.quantity == 0) {
if (data.quantity == 0) {
stock_detail = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock"% }</span>`;
} else {
stock_detail = `{% trans "Quantity" %}: ${data.quantity}`;
if (data.serial && data.quantity == 1) {
stock_detail = `{% trans "Serial Number" %}: ${data.serial}`;
} else {
stock_detail = `{% trans "Quantity" %}: ${data.quantity}`;
}
if (data.batch) {
stock_detail += ` - <small>{% trans "Batch" %}: ${data.batch}</small>`;
}
}
var html = `
<span>
${part_detail}
@ -192,7 +200,7 @@ function renderPart(name, data, parameters={}, options={}) {
<small>
${stock_data}
${extra}
${renderId('{% trans "Part ID" $}', data.pk, parameters)}
${renderId('{% trans "Part ID" %}', data.pk, parameters)}
</small>
</span>`;

View File

@ -171,6 +171,9 @@ function notificationCheck(force = false) {
{
success: function(response) {
updateNotificationIndicator(response.length);
},
error: function(xhr) {
console.warn('Could not access server: /api/notifications');
}
}
);

View File

@ -293,6 +293,7 @@ function categoryFields() {
return {
parent: {
help_text: '{% trans "Parent part category" %}',
required: false,
},
name: {},
description: {},
@ -373,6 +374,9 @@ function duplicatePart(pk, options={}) {
// Override the "variant_of" field
data.variant_of = pk;
// By default, disable "is_template" when making a variant *of* a template
data.is_template = false;
}
constructForm('{% url "api-part-list" %}', {

View File

@ -89,7 +89,8 @@ function updateSearch() {
var params = {};
if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) {
params.active = false;
// Return *only* active parts
params.active = true;
}
// Search for matching parts

View File

@ -107,6 +107,7 @@ function stockLocationFields(options={}) {
var fields = {
parent: {
help_text: '{% trans "Parent stock location" %}',
required: false,
},
name: {},
description: {},
@ -240,9 +241,11 @@ function stockItemFields(options={}) {
serial: {
icon: 'fa-hashtag',
},
batch: {
icon: 'fa-layer-group',
},
status: {},
expiry_date: {},
batch: {},
purchase_price: {
icon: 'fa-dollar-sign',
},
@ -963,6 +966,10 @@ function adjustStock(action, items, options={}) {
quantity = `#${item.serial}`;
}
if (item.batch) {
quantity += ` - <small>{% trans "Batch" %}: ${item.batch}</small>`;
}
var actionInput = '';
if (actionTitle != null) {
@ -1331,14 +1338,27 @@ function loadStockTestResultsTable(table, options) {
});
// Once the test template data are loaded, query for test results
var filters = loadTableFilters(filterKey);
var query_params = {
stock_item: options.stock_item,
user_detail: true,
attachment_detail: true,
ordering: '-date',
};
if ('result' in filters) {
query_params.result = filters.result;
}
if ('include_installed' in filters) {
query_params.include_installed = filters.include_installed;
}
inventreeGet(
'{% url "api-stock-test-result-list" %}',
{
stock_item: options.stock_item,
user_detail: true,
attachment_detail: true,
ordering: '-date',
},
query_params,
{
success: function(data) {
// Iterate through the returned test data
@ -1972,7 +1992,7 @@ function loadStockTable(table, options) {
var items = [];
selections.forEach(function(item) {
items.push(item.pk);
items.push(item);
});
scanItemsIntoLocation(items);
@ -2301,6 +2321,23 @@ function loadStockTrackingTable(table, options) {
var cols = [];
var filterTarget = '#filter-list-stocktracking';
var filterKey = 'stocktracking';
var filters = loadTableFilters(filterKey);
var params = options.params;
var original = {};
for (var k in params) {
original[k] = params[k];
filters[k] = params[k];
}
setupFilterList(filterKey, table, filterTarget);
// Date
cols.push({
field: 'date',
@ -2338,6 +2375,19 @@ function loadStockTrackingTable(table, options) {
return html;
}
// Part information
if (details.part) {
html += `<tr><th>{% trans "Part" %}</th><td>`;
if (details.part_detail) {
html += renderLink(details.part_detail.full_name, `/part/${details.part}/`);
} else {
html += `{% trans "Part information unavailable" %}`;
}
html += `</td></tr>`;
}
// Location information
if (details.location) {
@ -2475,27 +2525,10 @@ function loadStockTrackingTable(table, options) {
}
});
/*
// 2021-05-11 - Ability to edit or delete StockItemTracking entries is now removed
cols.push({
sortable: false,
formatter: function(value, row, index, field) {
// Manually created entries can be edited or deleted
if (false && !row.system) {
var bEdit = "<button title='{% trans 'Edit tracking entry' %}' class='btn btn-entry-edit btn-outline-secondary' type='button' url='/stock/track/" + row.pk + "/edit/'><span class='fas fa-edit'/></button>";
var bDel = "<button title='{% trans 'Delete tracking entry' %}' class='btn btn-entry-delete btn-outline-secondary' type='button' url='/stock/track/" + row.pk + "/delete/'><span class='fas fa-trash-alt icon-red'/></button>";
return "<div class='btn-group' role='group'>" + bEdit + bDel + "</div>";
} else {
return "";
}
}
});
*/
table.inventreeTable({
method: 'get',
queryParams: options.params,
queryParams: filters,
original: original,
columns: cols,
url: options.url,
});
@ -2626,7 +2659,8 @@ function installStockItem(stock_item_id, part_id, options={}) {
<ul>
<li>{% trans "The Stock Item links to a Part which is the BOM for this Stock Item" %}</li>
<li>{% trans "The Stock Item is currently available in stock" %}</li>
<li>{% trans "The Stock Item is serialized and does not belong to another item" %}</li>
<li>{% trans "The Stock Item is not already installed in another item" %}</li>
<li>{% trans "The Stock Item is tracked by either a batch code or serial number" %}</li>
</ul>
</div>`;
@ -2652,7 +2686,7 @@ function installStockItem(stock_item_id, part_id, options={}) {
filters: {
part_detail: true,
in_stock: true,
serialized: true,
tracked: true,
},
adjustFilters: function(filters, opts) {
var part = getFormFieldValue('part', {}, opts);

View File

@ -234,10 +234,19 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Stock status" %}',
description: '{% trans "Stock status" %}',
},
has_batch: {
title: '{% trans "Has batch code" %}',
type: 'bool',
},
batch: {
title: '{% trans "Batch" %}',
description: '{% trans "Batch code" %}',
},
tracked: {
title: '{% trans "Tracked" %}',
description: '{% trans "Stock item is tracked by either batch code or serial number" %}',
type: 'bool',
},
has_purchase_price: {
type: 'bool',
title: '{% trans "Has purchase price" %}',
@ -265,7 +274,16 @@ function getAvailableTableFilters(tableKey) {
// Filters for the 'stock test' table
if (tableKey == 'stocktests') {
return {};
return {
result: {
type: 'bool',
title: '{% trans "Test Passed" %}',
},
include_installed: {
type: 'bool',
title: '{% trans "Include Installed Items" %}',
}
};
}
// Filters for the 'part test template' table

View File

@ -0,0 +1,8 @@
{% load i18n %}
<button type='button' id='edit-notes' title='{% trans "Edit" %}' class='btn btn-primary'>
<span class='fas fa-edit'></span> {% trans "Edit" %}
</button>
<button type='button' id='save-notes' title='{% trans "Save" %}' class='btn btn-success' style='display: none;'>
<span class='fas fa-save'></span> {% trans "Save" %}
</button>

View File

@ -1,3 +1,5 @@
{% load i18n %}
<div class='panel' id='{{ panel_id }}'>
<div class='panel-heading'>
<h4>

View File

@ -1,5 +1,6 @@
# Please keep this list sorted
Django==3.2.12 # Django package
Django==3.2.12 # Django package
bleach==4.1.0 # HTML santization
certifi # Certifi is (most likely) installed through one of the requirements above
coreapi==2.3.0 # API documentation
coverage==5.3 # Unit test coverage