mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-2037
This commit is contained in:
commit
f9c004bd36
@ -7,7 +7,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from InvenTree.helpers import DownloadFile, GetExportFormats
|
from InvenTree.helpers import DownloadFile, GetExportFormats, normalize
|
||||||
|
|
||||||
from .admin import BomItemResource
|
from .admin import BomItemResource
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
uids = []
|
uids = []
|
||||||
|
|
||||||
def add_items(items, level):
|
def add_items(items, level, cascade):
|
||||||
# Add items at a given layer
|
# Add items at a given layer
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
@ -71,21 +71,13 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
bom_items.append(item)
|
bom_items.append(item)
|
||||||
|
|
||||||
if item.sub_part.assembly:
|
if cascade and item.sub_part.assembly:
|
||||||
if max_levels is None or level < max_levels:
|
if max_levels is None or level < max_levels:
|
||||||
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
|
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
|
||||||
|
|
||||||
if cascade:
|
top_level_items = part.get_bom_items().order_by('id')
|
||||||
# Cascading (multi-level) BOM
|
|
||||||
|
|
||||||
# Start with the top level
|
add_items(top_level_items, 1, cascade)
|
||||||
items_to_process = part.bom_items.all().order_by('id')
|
|
||||||
|
|
||||||
add_items(items_to_process, 1)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# No cascading needed - just the top-level items
|
|
||||||
bom_items = [item for item in part.bom_items.all().order_by('id')]
|
|
||||||
|
|
||||||
dataset = BomItemResource().export(queryset=bom_items, cascade=cascade)
|
dataset = BomItemResource().export(queryset=bom_items, cascade=cascade)
|
||||||
|
|
||||||
@ -148,8 +140,9 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
stock_data.append('')
|
stock_data.append('')
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
stock_data.append('')
|
stock_data.append('')
|
||||||
|
|
||||||
# Get part current stock
|
# Get part current stock
|
||||||
stock_data.append(str(bom_item.sub_part.available_stock))
|
stock_data.append(str(normalize(bom_item.sub_part.available_stock)))
|
||||||
|
|
||||||
for s_idx, header in enumerate(stock_headers):
|
for s_idx, header in enumerate(stock_headers):
|
||||||
try:
|
try:
|
||||||
|
@ -1392,6 +1392,27 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
|
return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
|
||||||
|
|
||||||
|
def get_installed_part_options(self, include_inherited=True, include_variants=True):
|
||||||
|
"""
|
||||||
|
Return a set of all Parts which can be "installed" into this part, based on the BOM.
|
||||||
|
|
||||||
|
arguments:
|
||||||
|
include_inherited - If set, include BomItem entries defined for parent parts
|
||||||
|
include_variants - If set, include variant parts for BomItems which allow variants
|
||||||
|
"""
|
||||||
|
|
||||||
|
parts = set()
|
||||||
|
|
||||||
|
for bom_item in self.get_bom_items(include_inherited=include_inherited):
|
||||||
|
|
||||||
|
if include_variants and bom_item.allow_variants:
|
||||||
|
for part in bom_item.sub_part.get_descendants(include_self=True):
|
||||||
|
parts.add(part)
|
||||||
|
else:
|
||||||
|
parts.add(bom_item.sub_part)
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
def get_used_in_filter(self, include_inherited=True):
|
def get_used_in_filter(self, include_inherited=True):
|
||||||
"""
|
"""
|
||||||
Return a query filter for all parts that this part is used in.
|
Return a query filter for all parts that this part is used in.
|
||||||
|
@ -117,6 +117,8 @@ class StockItemResource(ModelResource):
|
|||||||
exclude = [
|
exclude = [
|
||||||
# Exclude MPTT internal model fields
|
# Exclude MPTT internal model fields
|
||||||
'lft', 'rght', 'tree_id', 'level',
|
'lft', 'rght', 'tree_id', 'level',
|
||||||
|
# Exclude internal fields
|
||||||
|
'serial_int',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -876,6 +876,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
ordering_field_aliases = {
|
ordering_field_aliases = {
|
||||||
'SKU': 'supplier_part__SKU',
|
'SKU': 'supplier_part__SKU',
|
||||||
|
'stock': ['quantity', 'serial_int', 'serial'],
|
||||||
}
|
}
|
||||||
|
|
||||||
ordering_fields = [
|
ordering_fields = [
|
||||||
@ -887,6 +888,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
'stocktake_date',
|
'stocktake_date',
|
||||||
'expiry_date',
|
'expiry_date',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'stock',
|
||||||
'status',
|
'status',
|
||||||
'SKU',
|
'SKU',
|
||||||
]
|
]
|
||||||
|
18
InvenTree/stock/migrations/0068_stockitem_serial_int.py
Normal file
18
InvenTree/stock/migrations/0068_stockitem_serial_int.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-11-09 23:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0067_alter_stockitem_part'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='serial_int',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
54
InvenTree/stock/migrations/0069_auto_20211109_2347.py
Normal file
54
InvenTree/stock/migrations/0069_auto_20211109_2347.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 3.2.5 on 2021-11-09 23:47
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def update_serials(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Rebuild the integer serial number field for existing StockItem objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
StockItem = apps.get_model('stock', 'stockitem')
|
||||||
|
|
||||||
|
for item in StockItem.objects.all():
|
||||||
|
|
||||||
|
if item.serial is None:
|
||||||
|
# Skip items without existing serial numbers
|
||||||
|
continue
|
||||||
|
|
||||||
|
serial = 0
|
||||||
|
|
||||||
|
result = re.match(r"^(\d+)", str(item.serial))
|
||||||
|
|
||||||
|
if result and len(result.groups()) == 1:
|
||||||
|
try:
|
||||||
|
serial = int(result.groups()[0])
|
||||||
|
except:
|
||||||
|
serial = 0
|
||||||
|
|
||||||
|
|
||||||
|
item.serial_int = serial
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
def nupdate_serials(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Provided only for reverse migration compatibility
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0068_stockitem_serial_int'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
update_serials,
|
||||||
|
reverse_code=nupdate_serials,
|
||||||
|
)
|
||||||
|
]
|
@ -7,6 +7,7 @@ Stock database model definitions
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError, FieldError
|
from django.core.exceptions import ValidationError, FieldError
|
||||||
@ -223,6 +224,32 @@ class StockItem(MPTTModel):
|
|||||||
self.scheduled_for_deletion = True
|
self.scheduled_for_deletion = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def update_serial_number(self):
|
||||||
|
"""
|
||||||
|
Update the 'serial_int' field, to be an integer representation of the serial number.
|
||||||
|
This is used for efficient numerical sorting
|
||||||
|
"""
|
||||||
|
|
||||||
|
serial = getattr(self, 'serial', '')
|
||||||
|
|
||||||
|
# Default value if we cannot convert to an integer
|
||||||
|
serial_int = 0
|
||||||
|
|
||||||
|
if serial is not None:
|
||||||
|
|
||||||
|
serial = str(serial)
|
||||||
|
|
||||||
|
# Look at the start of the string - can it be "integerized"?
|
||||||
|
result = re.match(r'^(\d+)', serial)
|
||||||
|
|
||||||
|
if result and len(result.groups()) == 1:
|
||||||
|
try:
|
||||||
|
serial_int = int(result.groups()[0])
|
||||||
|
except:
|
||||||
|
serial_int = 0
|
||||||
|
|
||||||
|
self.serial_int = serial_int
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Save this StockItem to the database. Performs a number of checks:
|
Save this StockItem to the database. Performs a number of checks:
|
||||||
@ -234,6 +261,8 @@ class StockItem(MPTTModel):
|
|||||||
self.validate_unique()
|
self.validate_unique()
|
||||||
self.clean()
|
self.clean()
|
||||||
|
|
||||||
|
self.update_serial_number()
|
||||||
|
|
||||||
user = kwargs.pop('user', None)
|
user = kwargs.pop('user', None)
|
||||||
|
|
||||||
# If 'add_note = False' specified, then no tracking note will be added for item creation
|
# If 'add_note = False' specified, then no tracking note will be added for item creation
|
||||||
@ -504,6 +533,8 @@ class StockItem(MPTTModel):
|
|||||||
help_text=_('Serial number for this item')
|
help_text=_('Serial number for this item')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
serial_int = models.IntegerField(default=0)
|
||||||
|
|
||||||
link = InvenTreeURLField(
|
link = InvenTreeURLField(
|
||||||
verbose_name=_('External Link'),
|
verbose_name=_('External Link'),
|
||||||
max_length=125, blank=True,
|
max_length=125, blank=True,
|
||||||
|
@ -560,9 +560,8 @@ class StockItemInstall(AjaxUpdateView):
|
|||||||
|
|
||||||
# Filter for parts to install in this item
|
# Filter for parts to install in this item
|
||||||
if self.install_item:
|
if self.install_item:
|
||||||
# Get parts used in this part's BOM
|
# Get all parts which can be installed into this part
|
||||||
bom_items = self.part.get_bom_items()
|
allowed_parts = self.part.get_installed_part_options()
|
||||||
allowed_parts = [item.sub_part for item in bom_items]
|
|
||||||
# Filter
|
# Filter
|
||||||
items = items.filter(part__in=allowed_parts)
|
items = items.filter(part__in=allowed_parts)
|
||||||
|
|
||||||
|
@ -1128,7 +1128,9 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
col = {
|
col = {
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
|
sortName: 'stock',
|
||||||
title: '{% trans "Stock" %}',
|
title: '{% trans "Stock" %}',
|
||||||
|
sortable: true,
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
|
|
||||||
var val = parseFloat(value);
|
var val = parseFloat(value);
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
|
![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
|
||||||
![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
|
![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
|
||||||
![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
|
![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
|
||||||
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/InvenTree/InvenTree)
|
|
||||||
|
|
||||||
InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications.
|
InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications.
|
||||||
|
|
||||||
@ -17,6 +16,10 @@ InvenTree is designed to be lightweight and easy to use for SME or hobbyist appl
|
|||||||
|
|
||||||
However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
|
However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
|
||||||
|
|
||||||
|
# Demo
|
||||||
|
|
||||||
|
A demo instance of InvenTree is provided to allow users to explore the functionality of the software. [Read more here](https://inventree.readthedocs.io/en/latest/demo/)
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
|
|
||||||
[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree)
|
[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree)
|
||||||
|
Loading…
Reference in New Issue
Block a user