mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into plugin-mark-sample
This commit is contained in:
commit
544a590b7a
149
InvenTree/InvenTree/api_version.py
Normal file
149
InvenTree/InvenTree/api_version.py
Normal 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 ¶meters=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
|
||||
|
||||
"""
|
@ -667,6 +667,7 @@ LANGUAGES = [
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('fa', _('Farsi / Persian')),
|
||||
('fr', _('French')),
|
||||
('he', _('Hebrew')),
|
||||
('hu', _('Hungarian')),
|
||||
|
7
InvenTree/InvenTree/static/easymde/easymde.min.css
vendored
Normal file
7
InvenTree/InvenTree/static/easymde/easymde.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
InvenTree/InvenTree/static/easymde/easymde.min.js
vendored
Normal file
7
InvenTree/InvenTree/static/easymde/easymde.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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'),
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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 ¶meters=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 """
|
||||
|
@ -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() {
|
||||
|
@ -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
BIN
InvenTree/locale/fa/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/fa/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7845
InvenTree/locale/fa/LC_MESSAGES/django.po
Normal file
7845
InvenTree/locale/fa/LC_MESSAGES/django.po
Normal file
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
@ -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 %}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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 """
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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>
|
@ -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",
|
||||
|
@ -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 %}
|
@ -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',
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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
|
||||
|
@ -4,7 +4,6 @@
|
||||
{% load report %}
|
||||
{% load barcode %}
|
||||
{% load inventree_extras %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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>
|
||||
|
@ -89,7 +89,7 @@ $('table').find('.boolean-setting').change(function() {
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
onSuccess: function(data) {
|
||||
success: function(data) {
|
||||
},
|
||||
error: function(xhr) {
|
||||
showApiError(xhr, url);
|
||||
|
@ -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>
|
||||
|
@ -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:
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>`;
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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" %}', {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
8
InvenTree/templates/notes_buttons.html
Normal file
8
InvenTree/templates/notes_buttons.html
Normal 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>
|
@ -1,3 +1,5 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class='panel' id='{{ panel_id }}'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user