mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 03:55:41 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
25
.github/workflows/welcome.yml
vendored
25
.github/workflows/welcome.yml
vendored
@ -1,25 +0,0 @@
|
|||||||
# welcome new contributers
|
|
||||||
name: Welcome
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [ opened ]
|
|
||||||
issues:
|
|
||||||
types: [ opened ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/first-interaction@bd33205aa5c96838e10fd65df0d01efd613677c1 # pin@v1
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
issue-message: |
|
|
||||||
Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.
|
|
||||||
If you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).
|
|
||||||
pr-message: |
|
|
||||||
This is your first PR, welcome!
|
|
||||||
Please check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.
|
|
||||||
Make sure to document why this PR is needed and to link connected issues so we can review it faster.
|
|
@ -15,7 +15,7 @@ repos:
|
|||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
rev: '5.0.0'
|
rev: '5.0.4'
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [
|
additional_dependencies: [
|
||||||
|
@ -13,7 +13,7 @@ def isImportingData():
|
|||||||
return 'loaddata' in sys.argv
|
return 'loaddata' in sys.argv
|
||||||
|
|
||||||
|
|
||||||
def canAppAccessDatabase(allow_test=False):
|
def canAppAccessDatabase(allow_test: bool = False, allow_plugins: bool = False):
|
||||||
"""Returns True if the apps.py file can access database records.
|
"""Returns True if the apps.py file can access database records.
|
||||||
|
|
||||||
There are some circumstances where we don't want the ready function in apps.py
|
There are some circumstances where we don't want the ready function in apps.py
|
||||||
@ -25,8 +25,6 @@ def canAppAccessDatabase(allow_test=False):
|
|||||||
'flush',
|
'flush',
|
||||||
'loaddata',
|
'loaddata',
|
||||||
'dumpdata',
|
'dumpdata',
|
||||||
'makemigrations',
|
|
||||||
'migrate',
|
|
||||||
'check',
|
'check',
|
||||||
'shell',
|
'shell',
|
||||||
'createsuperuser',
|
'createsuperuser',
|
||||||
@ -43,6 +41,12 @@ def canAppAccessDatabase(allow_test=False):
|
|||||||
# Override for testing mode?
|
# Override for testing mode?
|
||||||
excluded_commands.append('test')
|
excluded_commands.append('test')
|
||||||
|
|
||||||
|
if not allow_plugins:
|
||||||
|
excluded_commands.extend([
|
||||||
|
'makemigrations',
|
||||||
|
'migrate',
|
||||||
|
])
|
||||||
|
|
||||||
for cmd in excluded_commands:
|
for cmd in excluded_commands:
|
||||||
if cmd in sys.argv:
|
if cmd in sys.argv:
|
||||||
return False
|
return False
|
||||||
|
@ -236,6 +236,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
help_text=_('Build status code')
|
help_text=_('Build status code')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_text(self):
|
||||||
|
"""Return the text representation of the status field"""
|
||||||
|
return BuildStatus.text(self.status)
|
||||||
|
|
||||||
batch = models.CharField(
|
batch = models.CharField(
|
||||||
verbose_name=_('Batch Code'),
|
verbose_name=_('Batch Code'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
|
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
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
@ -248,6 +248,11 @@ class PurchaseOrder(Order):
|
|||||||
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
|
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
|
||||||
help_text=_('Purchase order status'))
|
help_text=_('Purchase order status'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_text(self):
|
||||||
|
"""Return the text representation of the status field"""
|
||||||
|
return PurchaseOrderStatus.text(self.status)
|
||||||
|
|
||||||
supplier = models.ForeignKey(
|
supplier = models.ForeignKey(
|
||||||
Company, on_delete=models.SET_NULL,
|
Company, on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
@ -645,6 +650,11 @@ class SalesOrder(Order):
|
|||||||
status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(),
|
status = models.PositiveIntegerField(default=SalesOrderStatus.PENDING, choices=SalesOrderStatus.items(),
|
||||||
verbose_name=_('Status'), help_text=_('Purchase order status'))
|
verbose_name=_('Status'), help_text=_('Purchase order status'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_text(self):
|
||||||
|
"""Return the text representation of the status field"""
|
||||||
|
return SalesOrderStatus.text(self.status)
|
||||||
|
|
||||||
customer_reference = models.CharField(max_length=64, blank=True, verbose_name=_('Customer Reference '), help_text=_("Customer order reference code"))
|
customer_reference = models.CharField(max_length=64, blank=True, verbose_name=_('Customer Reference '), help_text=_("Customer order reference code"))
|
||||||
|
|
||||||
target_date = models.DateField(
|
target_date = models.DateField(
|
||||||
|
@ -286,6 +286,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% include "part/bom.html" with part=part %}
|
{% include "part/bom.html" with part=part %}
|
||||||
|
{% if roles.part.change %}
|
||||||
|
<button class='btn btn-success' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new-footer'>
|
||||||
|
<span class='fas fa-plus-circle'></span> {% trans "Add BOM Item" %}
|
||||||
|
</button>
|
||||||
|
<br/>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -611,7 +617,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#bom-item-new").click(function () {
|
$("[id^=bom-item-new]").click(function () {
|
||||||
|
|
||||||
var fields = bomItemFields();
|
var fields = bomItemFields();
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class PluginAppConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
"""The ready method is extended to initialize plugins."""
|
"""The ready method is extended to initialize plugins."""
|
||||||
if settings.PLUGINS_ENABLED:
|
if settings.PLUGINS_ENABLED:
|
||||||
if not canAppAccessDatabase(allow_test=True):
|
if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
|
||||||
logger.info("Skipping plugin loading sequence") # pragma: no cover
|
logger.info("Skipping plugin loading sequence") # pragma: no cover
|
||||||
else:
|
else:
|
||||||
logger.info('Loading InvenTree plugins')
|
logger.info('Loading InvenTree plugins')
|
||||||
|
@ -670,6 +670,11 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
|
|||||||
choices=StockStatus.items(),
|
choices=StockStatus.items(),
|
||||||
validators=[MinValueValidator(0)])
|
validators=[MinValueValidator(0)])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_text(self):
|
||||||
|
"""Return the text representation of the status field"""
|
||||||
|
return StockStatus.text(self.status)
|
||||||
|
|
||||||
notes = InvenTreeNotesField(help_text=_('Stock Item Notes'))
|
notes = InvenTreeNotesField(help_text=_('Stock Item Notes'))
|
||||||
|
|
||||||
purchase_price = InvenTreeModelMoneyField(
|
purchase_price = InvenTreeModelMoneyField(
|
||||||
|
@ -23,9 +23,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if setting.is_bool %}
|
{% if setting.is_bool %}
|
||||||
<div class='form-check form-switch'>
|
{% include "InvenTree/settings/setting_boolean.html" %}
|
||||||
<input class='form-check-input boolean-setting' fieldname='{{ setting.key.upper }}' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' id='setting-value-{{ setting.pk }}-{{ setting.typ }}' type='checkbox' {% if setting.as_bool %}checked=''{% endif %}{{reference}}>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id='setting-{{ setting.pk }}'>
|
<div id='setting-{{ setting.pk }}'>
|
||||||
<span id='setting-value-{{ setting.pk }}-{{ setting.typ }}' fieldname='{{ setting.key.upper }}'>
|
<span id='setting-value-{{ setting.pk }}-{{ setting.typ }}' fieldname='{{ setting.key.upper }}'>
|
||||||
@ -41,7 +39,18 @@
|
|||||||
</span>
|
</span>
|
||||||
{{ setting.units }}
|
{{ setting.units }}
|
||||||
<div class='btn-group float-right'>
|
<div class='btn-group float-right'>
|
||||||
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if plugin %}plugin='{{ plugin.slug }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
<button
|
||||||
|
class='btn btn-outline-secondary btn-small btn-edit-setting'
|
||||||
|
title='{% trans "Edit setting" %}'
|
||||||
|
pk='{{ setting.pk }}'
|
||||||
|
setting='{{ setting.key.upper }}'
|
||||||
|
{% if plugin %}plugin='{{ plugin.slug }}'{% endif %}
|
||||||
|
{% if user_setting or notification_setting %}user='{{request.user.id}}'{% endif %}
|
||||||
|
{% if notification_setting %}
|
||||||
|
notification=true
|
||||||
|
method='{{ setting.method }}'
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
18
InvenTree/templates/InvenTree/settings/setting_boolean.html
Normal file
18
InvenTree/templates/InvenTree/settings/setting_boolean.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class='form-check form-switch'>
|
||||||
|
<input
|
||||||
|
class='form-check-input boolean-setting'
|
||||||
|
fieldname='{{ setting.key.upper }}'
|
||||||
|
pk='{{ setting.pk }}'
|
||||||
|
setting='{{ setting.key.upper }}'
|
||||||
|
id='setting-value-{{setting.pk }}-{{ setting.typ }}'
|
||||||
|
type='checkbox'
|
||||||
|
{% if setting.as_bool %}checked=''{% endif %}
|
||||||
|
{{ reference }}
|
||||||
|
{% if plugin %}plugin='{{ plugin.slug }}'{% endif %}
|
||||||
|
{% if user_setting or notification_setting %}user='{{ request.user.pk }}'{% endif %}
|
||||||
|
{% if notification_setting %}
|
||||||
|
notification=true
|
||||||
|
method='{{ setting.method }}'
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
</div>
|
@ -78,12 +78,12 @@ $('table').find('.boolean-setting').change(function() {
|
|||||||
// Global setting by default
|
// Global setting by default
|
||||||
var url = `/api/settings/global/${setting}/`;
|
var url = `/api/settings/global/${setting}/`;
|
||||||
|
|
||||||
if (plugin) {
|
if (notification) {
|
||||||
|
url = `/api/settings/notification/${pk}/`;
|
||||||
|
} else if (plugin) {
|
||||||
url = `/api/plugin/settings/${plugin}/${setting}/`;
|
url = `/api/plugin/settings/${plugin}/${setting}/`;
|
||||||
} else if (user) {
|
} else if (user) {
|
||||||
url = `/api/settings/user/${setting}/`;
|
url = `/api/settings/user/${setting}/`;
|
||||||
} else if (notification) {
|
|
||||||
url = `/api/settings/notification/${pk}/`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inventreePut(
|
inventreePut(
|
||||||
|
@ -43,7 +43,9 @@ function makeBarcodeInput(placeholderText='', hintText='') {
|
|||||||
<span class='fas fa-qrcode'></span>
|
<span class='fas fa-qrcode'></span>
|
||||||
</span>
|
</span>
|
||||||
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
|
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
|
||||||
<button id='barcode_scan_btn' class='btn btn-secondary' onclick='onBarcodeScanClicked()' style='display: none;'><span class='fas fa-camera'></span></button>
|
<button id='barcode_scan_btn' type='button' class='btn btn-secondary' onclick='onBarcodeScanClicked()' style='display: none;'>
|
||||||
|
<span class='fas fa-camera'></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id='hint_barcode_data' class='help-block'>${hintText}</div>
|
<div id='hint_barcode_data' class='help-block'>${hintText}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -571,8 +573,13 @@ function barcodeCheckIn(location_id, options={}) {
|
|||||||
},
|
},
|
||||||
onScan: function(response) {
|
onScan: function(response) {
|
||||||
if ('stockitem' in response) {
|
if ('stockitem' in response) {
|
||||||
var stockitem = response.stockitem;
|
var pk = response.stockitem.pk;
|
||||||
|
|
||||||
|
inventreeGet(
|
||||||
|
`/api/stock/${pk}/`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
success: function(stockitem) {
|
||||||
var duplicate = false;
|
var duplicate = false;
|
||||||
|
|
||||||
items.forEach(function(item) {
|
items.forEach(function(item) {
|
||||||
@ -597,7 +604,9 @@ function barcodeCheckIn(location_id, options={}) {
|
|||||||
|
|
||||||
reloadTable();
|
reloadTable();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Barcode does not match a stock item
|
// Barcode does not match a stock item
|
||||||
showBarcodeMessage(modal, '{% trans "Barcode does not match Stock Item" %}', 'warning');
|
showBarcodeMessage(modal, '{% trans "Barcode does not match Stock Item" %}', 'warning');
|
||||||
@ -696,12 +705,26 @@ function scanItemsIntoLocation(item_list, options={}) {
|
|||||||
onScan: function(response) {
|
onScan: function(response) {
|
||||||
updateLocationInfo(null);
|
updateLocationInfo(null);
|
||||||
if ('stocklocation' in response) {
|
if ('stocklocation' in response) {
|
||||||
// Barcode corresponds to a StockLocation
|
|
||||||
stock_location = response.stocklocation;
|
var pk = response.stocklocation.pk;
|
||||||
|
|
||||||
|
inventreeGet(`/api/stock/location/${pk}/`, {}, {
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
stock_location = response;
|
||||||
|
|
||||||
updateLocationInfo(stock_location);
|
updateLocationInfo(stock_location);
|
||||||
modalEnable(modal, true);
|
modalEnable(modal, true);
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
// Barcode does *NOT* correspond to a StockLocation
|
||||||
|
showBarcodeMessage(
|
||||||
|
modal,
|
||||||
|
'{% trans "Barcode does not match a valid location" %}',
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Barcode does *NOT* correspond to a StockLocation
|
// Barcode does *NOT* correspond to a StockLocation
|
||||||
showBarcodeMessage(
|
showBarcodeMessage(
|
||||||
|
@ -971,7 +971,7 @@ function adjustStock(action, items, options={}) {
|
|||||||
|
|
||||||
var item = items[idx];
|
var item = items[idx];
|
||||||
|
|
||||||
if ((item.serial != null) && !allowSerializedStock) {
|
if ((item.serial != null) && (item.serial != '') && !allowSerializedStock) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1729,7 +1729,11 @@ function loadStockTable(table, options) {
|
|||||||
switchable: params['part_detail'],
|
switchable: params['part_detail'],
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
var ipn = row.part_detail.IPN;
|
var ipn = row.part_detail.IPN;
|
||||||
|
if (ipn) {
|
||||||
return withTitle(shortenString(ipn), ipn);
|
return withTitle(shortenString(ipn), ipn);
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ class InvenTreeUserAdmin(UserAdmin):
|
|||||||
|
|
||||||
(And it's confusing!)
|
(And it's confusing!)
|
||||||
"""
|
"""
|
||||||
|
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'last_login') # display last connection for each user in user admin panel.
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('username', 'password')}),
|
(None, {'fields': ('username', 'password')}),
|
||||||
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
|
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
|
||||||
|
@ -47,6 +47,9 @@ server {
|
|||||||
|
|
||||||
# Media files require user authentication
|
# Media files require user authentication
|
||||||
auth_request /auth;
|
auth_request /auth;
|
||||||
|
|
||||||
|
# Content header to force download
|
||||||
|
add_header Content-disposition "attachment";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Use the 'user' API endpoint for auth
|
# Use the 'user' API endpoint for auth
|
||||||
|
@ -46,6 +46,9 @@ server {
|
|||||||
|
|
||||||
# Media files require user authentication
|
# Media files require user authentication
|
||||||
auth_request /auth;
|
auth_request /auth;
|
||||||
|
|
||||||
|
# Content header to force download
|
||||||
|
add_header Content-disposition "attachment";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Use the 'user' API endpoint for auth
|
# Use the 'user' API endpoint for auth
|
||||||
|
Reference in New Issue
Block a user