mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
0213b550bc
5
.gitattributes
vendored
5
.gitattributes
vendored
@ -4,3 +4,8 @@
|
||||
*.md text
|
||||
*.html text
|
||||
*.txt text
|
||||
*.yml text
|
||||
*.yaml text
|
||||
*.conf text
|
||||
*.sh text
|
||||
*.js text
|
4
.github/workflows/coverage.yaml
vendored
4
.github/workflows/coverage.yaml
vendored
@ -29,8 +29,10 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke static
|
||||
- name: Coverage Tests
|
||||
run: |
|
||||
invoke coverage
|
||||
@ -42,6 +44,8 @@ jobs:
|
||||
rm test_db.sqlite
|
||||
invoke migrate
|
||||
invoke import-records -f data.json
|
||||
- name: Test Translations
|
||||
run: invoke translate
|
||||
- name: Check Migration Files
|
||||
run: python3 ci/check_migration_files.py
|
||||
- name: Upload Coverage Report
|
||||
|
31
.github/workflows/docker_build.yaml
vendored
31
.github/workflows/docker_build.yaml
vendored
@ -1,8 +1,11 @@
|
||||
# Test that the docker file builds correctly
|
||||
# Build and push latest docker image on push to master branch
|
||||
|
||||
name: Docker
|
||||
name: Docker Build
|
||||
|
||||
on: ["push", "pull_request"]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
|
||||
@ -10,9 +13,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build Server Image
|
||||
run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
|
||||
- name: Build nginx Image
|
||||
run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Login to Dockerhub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./docker
|
||||
push: true
|
||||
repository: inventree/inventree
|
||||
tags: inventree/inventree:latest
|
||||
- name: Image Digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
22
.github/workflows/docker_publish.yaml
vendored
22
.github/workflows/docker_publish.yaml
vendored
@ -7,12 +7,15 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
server_image:
|
||||
publish_image:
|
||||
name: Push InvenTree web server image to dockerhub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v2
|
||||
- name: cd
|
||||
run: |
|
||||
cd docker
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
@ -20,19 +23,4 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: inventree/inventree
|
||||
tag_with_ref: true
|
||||
dockerfile: docker/inventree/Dockerfile
|
||||
|
||||
nginx_image:
|
||||
name: Push InvenTree nginx image to dockerhub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: inventree/nginx
|
||||
tag_with_ref: true
|
||||
dockerfile: docker/nginx/Dockerfile
|
||||
dockerfile: ./Dockerfile
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -40,6 +40,7 @@ docs/_build
|
||||
# Local static and media file storage (only when running in development mode)
|
||||
inventree_media
|
||||
inventree_static
|
||||
static_i18n
|
||||
|
||||
# Local config file
|
||||
config.yaml
|
||||
|
61
InvenTree/InvenTree/management/commands/prerender.py
Normal file
61
InvenTree/InvenTree/management/commands/prerender.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""
|
||||
Custom management command to prerender files
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.module_loading import import_string
|
||||
from django.http.request import HttpRequest
|
||||
from django.utils.translation import override as lang_over
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def render_file(file_name, source, target, locales, ctx):
|
||||
""" renders a file into all provided locales """
|
||||
for locale in locales:
|
||||
target_file = os.path.join(target, locale + '.' + file_name)
|
||||
with open(target_file, 'w') as localised_file:
|
||||
with lang_over(locale):
|
||||
renderd = render_to_string(os.path.join(source, file_name), ctx)
|
||||
localised_file.write(renderd)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
django command to prerender files
|
||||
"""
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# static directories
|
||||
LC_DIR = settings.LOCALE_PATHS[0]
|
||||
SOURCE_DIR = settings.STATICFILES_I18_SRC
|
||||
TARGET_DIR = settings.STATICFILES_I18_TRG
|
||||
|
||||
# ensure static directory exists
|
||||
if not os.path.exists(TARGET_DIR):
|
||||
os.makedirs(TARGET_DIR, exist_ok=True)
|
||||
|
||||
# collect locales
|
||||
locales = {}
|
||||
for locale in os.listdir(LC_DIR):
|
||||
path = os.path.join(LC_DIR, locale)
|
||||
if os.path.exists(path) and os.path.isdir(path):
|
||||
locales[locale] = locale
|
||||
|
||||
# render!
|
||||
request = HttpRequest()
|
||||
ctx = {}
|
||||
processors = tuple(import_string(path) for path in settings.STATFILES_I18_PROCESSORS)
|
||||
for processor in processors:
|
||||
ctx.update(processor(request))
|
||||
|
||||
for file in os.listdir(SOURCE_DIR, ):
|
||||
path = os.path.join(SOURCE_DIR, file)
|
||||
if os.path.exists(path) and os.path.isfile(path):
|
||||
print(f"render {file}")
|
||||
render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx)
|
||||
else:
|
||||
raise NotImplementedError('Using multi-level directories is not implemented at this point') # TODO multilevel dir if needed
|
||||
print(f"rendered all files in {SOURCE_DIR}")
|
@ -52,6 +52,9 @@ def get_setting(environment_var, backup_val, default_value=None):
|
||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||
TESTING = 'test' in sys.argv
|
||||
|
||||
# New requirement for django 3.2+
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@ -190,6 +193,17 @@ STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'InvenTree', 'static'),
|
||||
]
|
||||
|
||||
# Translated Template settings
|
||||
STATICFILES_I18_PREFIX = 'i18n'
|
||||
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js')
|
||||
STATICFILES_I18_TRG = STATICFILES_DIRS[0] + '_' + STATICFILES_I18_PREFIX
|
||||
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
|
||||
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX)
|
||||
|
||||
STATFILES_I18_PROCESSORS = [
|
||||
'InvenTree.context.status_codes',
|
||||
]
|
||||
|
||||
# Color Themes Directory
|
||||
STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
|
||||
|
||||
@ -396,7 +410,6 @@ for key in db_keys:
|
||||
env_var = os.environ.get(env_key, None)
|
||||
|
||||
if env_var:
|
||||
logger.info(f"{env_key}={env_var}")
|
||||
# Override configuration value
|
||||
db_config[key] = env_var
|
||||
|
||||
@ -499,8 +512,8 @@ EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend'
|
||||
email_config = CONFIG.get('email', {})
|
||||
|
||||
EMAIL_BACKEND = get_setting(
|
||||
'django.core.mail.backends.smtp.EmailBackend',
|
||||
email_config.get('backend', '')
|
||||
'INVENTREE_EMAIL_BACKEND',
|
||||
email_config.get('backend', 'django.core.mail.backends.smtp.EmailBackend')
|
||||
)
|
||||
|
||||
# Email backend settings
|
||||
@ -524,6 +537,11 @@ EMAIL_HOST_PASSWORD = get_setting(
|
||||
email_config.get('password', ''),
|
||||
)
|
||||
|
||||
DEFAULT_FROM_EMAIL = get_setting(
|
||||
'INVENTREE_EMAIL_SENDER',
|
||||
email_config.get('sender', ''),
|
||||
)
|
||||
|
||||
EMAIL_SUBJECT_PREFIX = '[InvenTree] '
|
||||
|
||||
EMAIL_USE_LOCALTIME = False
|
||||
|
@ -185,6 +185,10 @@
|
||||
color: #c55;
|
||||
}
|
||||
|
||||
.icon-orange {
|
||||
color: #fcba03;
|
||||
}
|
||||
|
||||
.icon-green {
|
||||
color: #43bb43;
|
||||
}
|
||||
|
@ -56,17 +56,26 @@ def is_email_configured():
|
||||
configured = True
|
||||
|
||||
if not settings.EMAIL_HOST:
|
||||
logger.warning("EMAIL_HOST is not configured")
|
||||
configured = False
|
||||
|
||||
# Display warning unless in test mode
|
||||
if not settings.TESTING:
|
||||
logger.warning("EMAIL_HOST is not configured")
|
||||
|
||||
if not settings.EMAIL_HOST_USER:
|
||||
logger.warning("EMAIL_HOST_USER is not configured")
|
||||
configured = False
|
||||
|
||||
# Display warning unless in test mode
|
||||
if not settings.TESTING:
|
||||
logger.warning("EMAIL_HOST_USER is not configured")
|
||||
|
||||
if not settings.EMAIL_HOST_PASSWORD:
|
||||
logger.warning("EMAIL_HOST_PASSWORD is not configured")
|
||||
configured = False
|
||||
|
||||
# Display warning unless in test mode
|
||||
if not settings.TESTING:
|
||||
logger.warning("EMAIL_HOST_PASSWORD is not configured")
|
||||
|
||||
return configured
|
||||
|
||||
|
||||
|
@ -62,6 +62,14 @@ class StatusCode:
|
||||
def items(cls):
|
||||
return cls.options.items()
|
||||
|
||||
@classmethod
|
||||
def keys(cls):
|
||||
return cls.options.keys()
|
||||
|
||||
@classmethod
|
||||
def labels(cls):
|
||||
return cls.options.values()
|
||||
|
||||
@classmethod
|
||||
def label(cls, value):
|
||||
""" Return the status code label associated with the provided value """
|
||||
|
@ -8,7 +8,7 @@ import re
|
||||
|
||||
import common.models
|
||||
|
||||
INVENTREE_SW_VERSION = "0.2.1 pre"
|
||||
INVENTREE_SW_VERSION = "0.2.2 pre"
|
||||
|
||||
# Increment this number whenever there is a significant change to the API that any clients need to know about
|
||||
INVENTREE_API_VERSION = 2
|
||||
@ -19,6 +19,14 @@ def inventreeInstanceName():
|
||||
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
|
||||
|
||||
|
||||
def inventreeInstanceTitle():
|
||||
""" Returns the InstanceTitle for the current database """
|
||||
if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False):
|
||||
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
|
||||
else:
|
||||
return 'InvenTree'
|
||||
|
||||
|
||||
def inventreeVersion():
|
||||
""" Returns the InvenTree version string """
|
||||
return INVENTREE_SW_VERSION
|
||||
|
@ -11,7 +11,7 @@ from rest_framework import generics
|
||||
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.helpers import str2bool, isNull
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
|
||||
from .models import Build, BuildItem
|
||||
@ -194,7 +194,11 @@ class BuildItemList(generics.ListCreateAPIView):
|
||||
output = params.get('output', None)
|
||||
|
||||
if output:
|
||||
queryset = queryset.filter(install_into=output)
|
||||
|
||||
if isNull(output):
|
||||
queryset = queryset.filter(install_into=None)
|
||||
else:
|
||||
queryset = queryset.filter(install_into=output)
|
||||
|
||||
return queryset
|
||||
|
||||
|
@ -3,6 +3,7 @@ Django Forms for interacting with Build objects
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -12,6 +13,8 @@ from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
from InvenTree.fields import DatePickerFormField
|
||||
|
||||
from InvenTree.status_codes import StockStatus
|
||||
|
||||
from .models import Build, BuildItem, BuildOrderAttachment
|
||||
|
||||
from stock.models import StockLocation, StockItem
|
||||
@ -165,16 +168,10 @@ class AutoAllocateForm(HelperForm):
|
||||
|
||||
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm stock allocation'))
|
||||
|
||||
# Keep track of which build output we are interested in
|
||||
output = forms.ModelChoiceField(
|
||||
queryset=StockItem.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
'confirm',
|
||||
'output',
|
||||
]
|
||||
|
||||
|
||||
@ -214,6 +211,13 @@ class CompleteBuildOutputForm(HelperForm):
|
||||
help_text=_('Location of completed parts'),
|
||||
)
|
||||
|
||||
stock_status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
help_text=_('Build output stock status'),
|
||||
initial=StockStatus.OK,
|
||||
choices=StockStatus.items(),
|
||||
)
|
||||
|
||||
confirm_incomplete = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Confirm incomplete'),
|
||||
@ -232,10 +236,15 @@ class CompleteBuildOutputForm(HelperForm):
|
||||
fields = [
|
||||
'location',
|
||||
'output',
|
||||
'stock_status',
|
||||
'confirm',
|
||||
'confirm_incomplete',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class CancelBuildForm(HelperForm):
|
||||
""" Form for cancelling a build """
|
||||
|
@ -22,7 +22,7 @@ from markdownx.models import MarkdownxField
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
||||
from InvenTree.validators import validate_build_order_reference
|
||||
from InvenTree.models import InvenTreeAttachment
|
||||
@ -314,6 +314,42 @@ class Build(MPTTModel):
|
||||
'sub_part'
|
||||
)
|
||||
|
||||
@property
|
||||
def tracked_bom_items(self):
|
||||
"""
|
||||
Returns the "trackable" BOM items for this BuildOrder
|
||||
"""
|
||||
|
||||
items = self.bom_items
|
||||
items = items.filter(sub_part__trackable=True)
|
||||
|
||||
return items
|
||||
|
||||
def has_tracked_bom_items(self):
|
||||
"""
|
||||
Returns True if this BuildOrder has trackable BomItems
|
||||
"""
|
||||
|
||||
return self.tracked_bom_items.count() > 0
|
||||
|
||||
@property
|
||||
def untracked_bom_items(self):
|
||||
"""
|
||||
Returns the "non trackable" BOM items for this BuildOrder
|
||||
"""
|
||||
|
||||
items = self.bom_items
|
||||
items = items.filter(sub_part__trackable=False)
|
||||
|
||||
return items
|
||||
|
||||
def has_untracked_bom_items(self):
|
||||
"""
|
||||
Returns True if this BuildOrder has non trackable BomItems
|
||||
"""
|
||||
|
||||
return self.untracked_bom_items.count() > 0
|
||||
|
||||
@property
|
||||
def remaining(self):
|
||||
"""
|
||||
@ -449,6 +485,9 @@ class Build(MPTTModel):
|
||||
if self.completed < self.quantity:
|
||||
return False
|
||||
|
||||
if not self.areUntrackedPartsFullyAllocated():
|
||||
return False
|
||||
|
||||
# No issues!
|
||||
return True
|
||||
|
||||
@ -458,7 +497,7 @@ class Build(MPTTModel):
|
||||
Mark this build as complete
|
||||
"""
|
||||
|
||||
if not self.can_complete:
|
||||
if self.incomplete_count > 0:
|
||||
return
|
||||
|
||||
self.completion_date = datetime.now().date()
|
||||
@ -466,6 +505,9 @@ class Build(MPTTModel):
|
||||
self.status = BuildStatus.COMPLETE
|
||||
self.save()
|
||||
|
||||
# Remove untracked allocated stock
|
||||
self.subtractUntrackedStock(user)
|
||||
|
||||
# Ensure that there are no longer any BuildItem objects
|
||||
# which point to thie Build Order
|
||||
self.allocated_stock.all().delete()
|
||||
@ -489,7 +531,7 @@ class Build(MPTTModel):
|
||||
self.status = BuildStatus.CANCELLED
|
||||
self.save()
|
||||
|
||||
def getAutoAllocations(self, output):
|
||||
def getAutoAllocations(self):
|
||||
"""
|
||||
Return a list of StockItem objects which will be allocated
|
||||
using the 'AutoAllocate' function.
|
||||
@ -521,15 +563,19 @@ class Build(MPTTModel):
|
||||
|
||||
part = bom_item.sub_part
|
||||
|
||||
# If the part is "trackable" it cannot be auto-allocated
|
||||
if part.trackable:
|
||||
continue
|
||||
|
||||
# Skip any parts which are already fully allocated
|
||||
if self.isPartFullyAllocated(part, output):
|
||||
if self.isPartFullyAllocated(part, None):
|
||||
continue
|
||||
|
||||
# How many parts are required to complete the output?
|
||||
required = self.unallocatedQuantity(part, output)
|
||||
required = self.unallocatedQuantity(part, None)
|
||||
|
||||
# Grab a list of stock items which are available
|
||||
stock_items = self.availableStockItems(part, output)
|
||||
stock_items = self.availableStockItems(part, None)
|
||||
|
||||
# Ensure that the available stock items are in the correct location
|
||||
if self.take_from is not None:
|
||||
@ -544,7 +590,6 @@ class Build(MPTTModel):
|
||||
build_items = BuildItem.objects.filter(
|
||||
build=self,
|
||||
stock_item=stock_item,
|
||||
install_into=output
|
||||
)
|
||||
|
||||
if len(build_items) > 0:
|
||||
@ -567,24 +612,45 @@ class Build(MPTTModel):
|
||||
return allocations
|
||||
|
||||
@transaction.atomic
|
||||
def unallocateStock(self, output=None, part=None):
|
||||
def unallocateOutput(self, output, part=None):
|
||||
"""
|
||||
Deletes all stock allocations for this build.
|
||||
|
||||
Args:
|
||||
output: Specify which build output to delete allocations (optional)
|
||||
|
||||
Unallocate all stock which are allocated against the provided "output" (StockItem)
|
||||
"""
|
||||
|
||||
allocations = BuildItem.objects.filter(build=self.pk)
|
||||
|
||||
if output:
|
||||
allocations = allocations.filter(install_into=output.pk)
|
||||
allocations = BuildItem.objects.filter(
|
||||
build=self,
|
||||
install_into=output
|
||||
)
|
||||
|
||||
if part:
|
||||
allocations = allocations.filter(stock_item__part=part)
|
||||
|
||||
# Remove all the allocations
|
||||
allocations.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def unallocateUntracked(self, part=None):
|
||||
"""
|
||||
Unallocate all "untracked" stock
|
||||
"""
|
||||
|
||||
allocations = BuildItem.objects.filter(
|
||||
build=self,
|
||||
install_into=None
|
||||
)
|
||||
|
||||
if part:
|
||||
allocations = allocations.filter(stock_item__part=part)
|
||||
|
||||
allocations.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def unallocateAll(self):
|
||||
"""
|
||||
Deletes all stock allocations for this build.
|
||||
"""
|
||||
|
||||
allocations = BuildItem.objects.filter(build=self)
|
||||
|
||||
allocations.delete()
|
||||
|
||||
@transaction.atomic
|
||||
@ -679,13 +745,13 @@ class Build(MPTTModel):
|
||||
raise ValidationError(_("Build output does not match Build Order"))
|
||||
|
||||
# Unallocate all build items against the output
|
||||
self.unallocateStock(output)
|
||||
self.unallocateOutput(output)
|
||||
|
||||
# Remove the build output from the database
|
||||
output.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def autoAllocate(self, output):
|
||||
def autoAllocate(self):
|
||||
"""
|
||||
Run auto-allocation routine to allocate StockItems to this Build.
|
||||
|
||||
@ -702,7 +768,7 @@ class Build(MPTTModel):
|
||||
See: getAutoAllocations()
|
||||
"""
|
||||
|
||||
allocations = self.getAutoAllocations(output)
|
||||
allocations = self.getAutoAllocations()
|
||||
|
||||
for item in allocations:
|
||||
# Create a new allocation
|
||||
@ -710,11 +776,29 @@ class Build(MPTTModel):
|
||||
build=self,
|
||||
stock_item=item['stock_item'],
|
||||
quantity=item['quantity'],
|
||||
install_into=output,
|
||||
install_into=None
|
||||
)
|
||||
|
||||
build_item.save()
|
||||
|
||||
@transaction.atomic
|
||||
def subtractUntrackedStock(self, user):
|
||||
"""
|
||||
Called when the Build is marked as "complete",
|
||||
this function removes the allocated untracked items from stock.
|
||||
"""
|
||||
|
||||
items = self.allocated_stock.filter(
|
||||
stock_item__part__trackable=False
|
||||
)
|
||||
|
||||
# Remove stock
|
||||
for item in items:
|
||||
item.complete_allocation(user)
|
||||
|
||||
# Delete allocation
|
||||
items.all().delete()
|
||||
|
||||
@transaction.atomic
|
||||
def completeBuildOutput(self, output, user, **kwargs):
|
||||
"""
|
||||
@ -726,6 +810,7 @@ class Build(MPTTModel):
|
||||
|
||||
# Select the location for the build output
|
||||
location = kwargs.get('location', self.destination)
|
||||
status = kwargs.get('status', StockStatus.OK)
|
||||
|
||||
# List the allocated BuildItem objects for the given output
|
||||
allocated_items = output.items_to_install.all()
|
||||
@ -733,9 +818,7 @@ class Build(MPTTModel):
|
||||
for build_item in allocated_items:
|
||||
|
||||
# TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
|
||||
# TODO: Use celery / redis to offload the actual object deletion...
|
||||
# REF: https://www.botreetechnologies.com/blog/implementing-celery-using-django-for-background-task-processing
|
||||
# REF: https://code.tutsplus.com/tutorials/using-celery-with-django-for-background-task-processing--cms-28732
|
||||
# TODO: Use the background worker process to handle this task!
|
||||
|
||||
# Complete the allocation of stock for that item
|
||||
build_item.complete_allocation(user)
|
||||
@ -747,6 +830,7 @@ class Build(MPTTModel):
|
||||
output.build = self
|
||||
output.is_building = False
|
||||
output.location = location
|
||||
output.status = status
|
||||
|
||||
output.save()
|
||||
|
||||
@ -779,7 +863,7 @@ class Build(MPTTModel):
|
||||
if output:
|
||||
quantity *= output.quantity
|
||||
else:
|
||||
quantity *= self.remaining
|
||||
quantity *= self.quantity
|
||||
|
||||
return quantity
|
||||
|
||||
@ -807,7 +891,13 @@ class Build(MPTTModel):
|
||||
|
||||
allocations = self.allocatedItems(part, output)
|
||||
|
||||
allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0))
|
||||
allocated = allocations.aggregate(
|
||||
q=Coalesce(
|
||||
Sum('quantity'),
|
||||
0,
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
return allocated['q']
|
||||
|
||||
@ -828,19 +918,39 @@ class Build(MPTTModel):
|
||||
|
||||
return self.unallocatedQuantity(part, output) == 0
|
||||
|
||||
def isFullyAllocated(self, output):
|
||||
def isFullyAllocated(self, output, verbose=False):
|
||||
"""
|
||||
Returns True if the particular build output is fully allocated.
|
||||
"""
|
||||
|
||||
for bom_item in self.bom_items:
|
||||
# If output is not specified, we are talking about "untracked" items
|
||||
if output is None:
|
||||
bom_items = self.untracked_bom_items
|
||||
else:
|
||||
bom_items = self.tracked_bom_items
|
||||
|
||||
fully_allocated = True
|
||||
|
||||
for bom_item in bom_items:
|
||||
part = bom_item.sub_part
|
||||
|
||||
if not self.isPartFullyAllocated(part, output):
|
||||
return False
|
||||
fully_allocated = False
|
||||
|
||||
if verbose:
|
||||
print(f"Part {part} is not fully allocated for output {output}")
|
||||
else:
|
||||
break
|
||||
|
||||
# All parts must be fully allocated!
|
||||
return True
|
||||
return fully_allocated
|
||||
|
||||
def areUntrackedPartsFullyAllocated(self):
|
||||
"""
|
||||
Returns True if the un-tracked parts are fully allocated for this BuildOrder
|
||||
"""
|
||||
|
||||
return self.isFullyAllocated(None)
|
||||
|
||||
def allocatedParts(self, output):
|
||||
"""
|
||||
@ -849,7 +959,13 @@ class Build(MPTTModel):
|
||||
|
||||
allocated = []
|
||||
|
||||
for bom_item in self.bom_items:
|
||||
# If output is not specified, we are talking about "untracked" items
|
||||
if output is None:
|
||||
bom_items = self.untracked_bom_items
|
||||
else:
|
||||
bom_items = self.tracked_bom_items
|
||||
|
||||
for bom_item in bom_items:
|
||||
part = bom_item.sub_part
|
||||
|
||||
if self.isPartFullyAllocated(part, output):
|
||||
@ -864,7 +980,13 @@ class Build(MPTTModel):
|
||||
|
||||
unallocated = []
|
||||
|
||||
for bom_item in self.bom_items:
|
||||
# If output is not specified, we are talking about "untracked" items
|
||||
if output is None:
|
||||
bom_items = self.untracked_bom_items
|
||||
else:
|
||||
bom_items = self.tracked_bom_items
|
||||
|
||||
for bom_item in bom_items:
|
||||
part = bom_item.sub_part
|
||||
|
||||
if not self.isPartFullyAllocated(part, output):
|
||||
@ -1014,10 +1136,12 @@ class BuildItem(models.Model):
|
||||
|
||||
errors = {}
|
||||
|
||||
if not self.install_into:
|
||||
raise ValidationError(_('Build item must specify a build output'))
|
||||
|
||||
try:
|
||||
|
||||
# If the 'part' is trackable, then the 'install_into' field must be set!
|
||||
if self.stock_item.part and self.stock_item.part.trackable and not self.install_into:
|
||||
raise ValidationError(_('Build item must specify a build output, as master part is marked as trackable'))
|
||||
|
||||
# Allocated part must be in the BOM for the master part
|
||||
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
|
||||
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
|
||||
|
@ -4,7 +4,7 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | Allocate Parts
|
||||
{% inventree_title %} | {% trans "Allocate Parts" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block menubar %}
|
||||
@ -12,48 +12,41 @@ InvenTree | Allocate Parts
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Incomplete Build Ouputs" %}
|
||||
{% trans "Allocate Stock to Build" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% if build.is_complete %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Build order has been completed" %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if build.has_untracked_bom_items %}
|
||||
{% if build.active %}
|
||||
<div class='btn-group' role='group'>
|
||||
{% if build.active %}
|
||||
<button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
|
||||
<button class='btn btn-success' type='button' id='btn-auto-allocate' title='{% trans "Allocate stock to build" %}'>
|
||||
<span class='fas fa-magic'></span> {% trans "Auto Allocate" %}
|
||||
</button>
|
||||
<!--
|
||||
<button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
||||
</button>
|
||||
-->
|
||||
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'>
|
||||
<span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!--
|
||||
<button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
||||
</button>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
{% if build.incomplete_outputs %}
|
||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||
{% for item in build.incomplete_outputs %}
|
||||
{% include "build/allocation_card.html" with item=item %}
|
||||
{% endfor %}
|
||||
{% if build.areUntrackedPartsFullyAllocated %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Untracked stock has been fully allocated for this Build Order" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<b>{% trans "Create a new build output" %}</b><br>
|
||||
{% trans "No incomplete build outputs remain." %}<br>
|
||||
{% trans "Create a new build output using the button above" %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Untracked stock has not been fully allocated for this Build Order" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<table class='table table-striped table-condensed' id='allocation-table-untracked'></table>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This Build Order does not have any associated untracked BOM items" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
@ -66,19 +59,17 @@ InvenTree | Allocate Parts
|
||||
part: {{ build.part.pk }},
|
||||
};
|
||||
|
||||
{% for item in build.incomplete_outputs %}
|
||||
// Get the build output as a javascript object
|
||||
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
||||
{
|
||||
success: function(response) {
|
||||
loadBuildOutputAllocationTable(buildInfo, response);
|
||||
}
|
||||
}
|
||||
);
|
||||
{% endfor %}
|
||||
{% if build.has_untracked_bom_items %}
|
||||
// Load allocation table for un-tracked parts
|
||||
loadBuildOutputAllocationTable(buildInfo, null);
|
||||
{% endif %}
|
||||
|
||||
function reloadTable() {
|
||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
{% if build.active %}
|
||||
$("#btn-allocate").on('click', function() {
|
||||
$("#btn-auto-allocate").on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'build-auto-allocate' build.id %}",
|
||||
{
|
||||
@ -86,20 +77,12 @@ InvenTree | Allocate Parts
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
$('#btn-unallocate').on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'build-unallocate' build.id %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#btn-create-output').click(function() {
|
||||
launchModalForm('{% url "build-output-create" build.id %}',
|
||||
{
|
||||
reload: true,
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -7,23 +7,31 @@
|
||||
<div class="panel-heading" role="tab" id="heading-{{ pk }}">
|
||||
<div class="panel-title">
|
||||
<div class='row'>
|
||||
{% if tracked_items %}
|
||||
<a class='collapsed' aria-expanded='false' role="button" data-toggle="collapse" data-parent="#build-output-accordion" href="#collapse-{{ pk }}" aria-controls="collapse-{{ pk }}">
|
||||
{% endif %}
|
||||
<div class='col-sm-4'>
|
||||
{% if tracked_items %}
|
||||
<span class='fas fa-caret-right'></span>
|
||||
{% endif %}
|
||||
{{ item.part.full_name }}
|
||||
</div>
|
||||
<div class='col-sm-2'>
|
||||
{% if item.serial %}
|
||||
# {{ item.serial }}
|
||||
{% trans "Serial Number" %}: {{ item.serial }}
|
||||
{% else %}
|
||||
{% decimal item.quantity %}
|
||||
{% trans "Quantity" %}: {% decimal item.quantity %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if tracked_items %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class='col-sm-3'>
|
||||
<div>
|
||||
<div id='output-progress-{{ pk }}'>
|
||||
{% if tracked_items %}
|
||||
<span class='fas fa-spin fa-spinner'></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,24 +3,52 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load status_codes %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Build Order" %} - {{ build }}
|
||||
{% inventree_title %} | {% trans "Build Order" %} - {{ build }}
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content %}
|
||||
{% block header_pre_content %}
|
||||
{% if build.sales_order %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This Build Order is allocated to Sales Order" %} <b><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></b>
|
||||
{% object_link 'so-detail' build.sales_order.id build.sales_order as link %}
|
||||
{% blocktrans %}This Build Order is allocated to Sales Order {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.parent %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This Build Order is a child of Build Order" %} <b><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a></b>
|
||||
{% object_link 'build-detail' build.parent.id build.parent as link %}
|
||||
{% blocktrans %}This Build Order is a child of Build Order {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header_post_content %}
|
||||
{% if build.active %}
|
||||
{% if build.can_complete %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Build Order is ready to mark as completed" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.incomplete_count > 0 %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Build Order cannot be completed as outstanding outputs remain" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.completed < build.quantity %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Required build quantity has not yet been completed" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not build.areUntrackedPartsFullyAllocated %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Stock has not been fully allocated to this Build Order" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class="part-thumb"
|
||||
{% if build.part.image %}
|
||||
@ -58,6 +86,11 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</div>
|
||||
<!-- Build actions -->
|
||||
{% if roles.build.change %}
|
||||
{% if build.active %}
|
||||
<button id='build-complete' title='{% trans "Complete Build" %}' class='btn btn-success'>
|
||||
<span class='fas fa-paper-plane'></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class='btn-group'>
|
||||
<button id='build-options' title='{% trans "Build actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
||||
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||
@ -65,12 +98,11 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<ul class='dropdown-menu' role='menu'>
|
||||
<li><a href='#' id='build-edit'><span class='fas fa-edit icon-green'></span> {% trans "Edit Build" %}</a></li>
|
||||
{% if build.is_active %}
|
||||
<li><a href='#' id='build-complete'><span class='fas fa-tools'></span> {% trans "Complete Build" %}</a></li>
|
||||
<li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
|
||||
{% endif %}
|
||||
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
||||
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -105,7 +137,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>
|
||||
{{ build.target_date }}
|
||||
{% if build.is_overdue %}
|
||||
<span title='{% trans "This build was due on" %} {{ build.target_date }}' class='label label-red'>{% trans "Overdue" %}</span>
|
||||
<span title='{% blocktrans with target=build.target_date %}This build was due on {{target}}{% endblocktrans %}' class='label label-red'>{% trans "Overdue" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -169,6 +201,13 @@ src="{% static 'img/blank_image.png' %}"
|
||||
});
|
||||
|
||||
$("#build-complete").on('click', function() {
|
||||
|
||||
{% if build.incomplete_count > 0 %}
|
||||
showAlertDialog(
|
||||
'{% trans "Incomplete Outputs" %}',
|
||||
'{% trans "Build Order cannot be completed as incomplete build outputs remain" %}'
|
||||
);
|
||||
{% else %}
|
||||
launchModalForm(
|
||||
"{% url 'build-complete' build.id %}",
|
||||
{
|
||||
@ -176,6 +215,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
submit_text: '{% trans "Complete Build" %}',
|
||||
}
|
||||
);
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
$('#print-build-report').click(function() {
|
||||
|
@ -6,19 +6,68 @@
|
||||
{% include "build/navbar.html" with tab='output' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Build Outputs" %}
|
||||
{% endblock %}
|
||||
{% block content_panels %}
|
||||
|
||||
{% block details %}
|
||||
{% if not build.is_complete %}
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% trans "Incomplete Build Outputs" %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{% include "stock_table.html" with read_only=True %}
|
||||
<div class='panel-content'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if build.active %}
|
||||
<button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if build.incomplete_outputs %}
|
||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||
{% for item in build.incomplete_outputs %}
|
||||
{% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<b>{% trans "Create a new build output" %}</b><br>
|
||||
{% trans "No incomplete build outputs remain." %}<br>
|
||||
{% trans "Create a new build output using the button above" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% trans "Completed Build Outputs" %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
{% include "stock_table.html" with read_only=True %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#btn-create-output').click(function() {
|
||||
launchModalForm('{% url "build-output-create" build.id %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
loadStockTable($("#stock-table"), {
|
||||
params: {
|
||||
location_detail: true,
|
||||
@ -32,4 +81,23 @@ loadStockTable($("#stock-table"), {
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
});
|
||||
|
||||
var buildInfo = {
|
||||
pk: {{ build.pk }},
|
||||
quantity: {{ build.quantity }},
|
||||
completed: {{ build.completed }},
|
||||
part: {{ build.part.pk }},
|
||||
};
|
||||
|
||||
{% for item in build.incomplete_outputs %}
|
||||
// Get the build output as a javascript object
|
||||
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
||||
{
|
||||
success: function(response) {
|
||||
loadBuildOutputAllocationTable(buildInfo, response);
|
||||
}
|
||||
}
|
||||
);
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% endblock %}
|
@ -5,11 +5,11 @@
|
||||
|
||||
{% if build.can_complete %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Build can be completed" %}
|
||||
{% trans "Build Order is complete" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
<b>{% trans "Build cannot be completed" %}</b><br>
|
||||
<b>{% trans "Build Order is incomplete" %}</b><br>
|
||||
<ul>
|
||||
{% if build.incomplete_count > 0 %}
|
||||
<li>{% trans "Incompleted build outputs remain" %}</li>
|
||||
@ -17,6 +17,9 @@
|
||||
{% if build.completed < build.quantity %}
|
||||
<li>{% trans "Required build quantity has not been completed" %}</li>
|
||||
{% endif %}
|
||||
{% if not build.areUntrackedPartsFullyAllocated %}
|
||||
<li>{% trans "Required stock has not been fully allocated" %}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -4,9 +4,10 @@
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% if fully_allocated %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<h4>{% trans "Stock allocation is complete" %}</h4>
|
||||
{% if not build.has_tracked_bom_items %}
|
||||
{% elif fully_allocated %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Stock allocation is complete for this output" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
@ -16,7 +17,7 @@
|
||||
<div class='panel panel-default'>
|
||||
<div class='panel panel-heading'>
|
||||
<a data-toggle='collapse' href='#collapse-unallocated'>
|
||||
{{ unallocated_parts|length }} {% trans "parts have not been fully allocated" %}
|
||||
{{ unallocated_parts|length }} {% trans "tracked parts have not been fully allocated" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class='panel-collapse collapse' id='collapse-unallocated'>
|
||||
@ -41,7 +42,11 @@
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
||||
{% if output.serialized %}
|
||||
{{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
|
||||
{% else %}
|
||||
{% decimal output.quantity %} x {{ output.part.full_name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -8,15 +8,13 @@
|
||||
</p>
|
||||
{% if output %}
|
||||
<p>
|
||||
{% trans "The allocated stock will be installed into the following build output:" %}
|
||||
<br>
|
||||
<i>{{ output }}</i>
|
||||
{% blocktrans %}The allocated stock will be installed into the following build output:<br><i>{{output}}</i>{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if no_stock %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "No stock available for" %} {{ part }}
|
||||
{% blocktrans %}No stock available for {{part}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -5,7 +5,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Build Orders" %}
|
||||
{% inventree_title %} | {% trans "Build Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -17,17 +17,11 @@
|
||||
</li>
|
||||
|
||||
{% if build.active %}
|
||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Required Parts" %}'>
|
||||
<a href='{% url "build-parts" build.id %}'>
|
||||
<span class='fas fa-shapes'></span>
|
||||
{% trans "Required Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "In Progress" %}'>
|
||||
|
||||
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "Allocate Stock" %}'>
|
||||
<a href='{% url "build-allocate" build.id %}'>
|
||||
<span class='fas fa-tools'></span>
|
||||
{% trans "In Progress" %}
|
||||
{% trans "Allocate Stock" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
@ -1,30 +0,0 @@
|
||||
{% extends "build/build_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load status_codes %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "build/navbar.html" with tab='parts' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Required Parts" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<table class='table table-striped table-condensed' id='parts-table'></table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
loadBuildPartsTable($('#parts-table'), {
|
||||
part: {{ build.part.pk }},
|
||||
build: {{ build.pk }},
|
||||
build_quantity: {{ build.quantity }},
|
||||
build_remaining: {{ build.remaining }},
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -19,6 +19,18 @@ class BuildTest(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
Initialize data to use for these tests.
|
||||
|
||||
The base Part 'assembly' has a BOM consisting of three parts:
|
||||
|
||||
- 5 x sub_part_1
|
||||
- 3 x sub_part_2
|
||||
- 2 x sub_part_3 (trackable)
|
||||
|
||||
We will build 10x 'assembly' parts, in two build outputs:
|
||||
|
||||
- 3 x output_1
|
||||
- 7 x output_2
|
||||
|
||||
"""
|
||||
|
||||
# Create a base "Part"
|
||||
@ -41,17 +53,31 @@ class BuildTest(TestCase):
|
||||
component=True
|
||||
)
|
||||
|
||||
self.sub_part_3 = Part.objects.create(
|
||||
name="Widget C",
|
||||
description="A widget",
|
||||
component=True,
|
||||
trackable=True
|
||||
)
|
||||
|
||||
# Create BOM item links for the parts
|
||||
BomItem.objects.create(
|
||||
part=self.assembly,
|
||||
sub_part=self.sub_part_1,
|
||||
quantity=10
|
||||
quantity=5
|
||||
)
|
||||
|
||||
BomItem.objects.create(
|
||||
part=self.assembly,
|
||||
sub_part=self.sub_part_2,
|
||||
quantity=25
|
||||
quantity=3
|
||||
)
|
||||
|
||||
# sub_part_3 is trackable!
|
||||
BomItem.objects.create(
|
||||
part=self.assembly,
|
||||
sub_part=self.sub_part_3,
|
||||
quantity=2
|
||||
)
|
||||
|
||||
# Create a "Build" object to make 10x objects
|
||||
@ -64,14 +90,14 @@ class BuildTest(TestCase):
|
||||
# Create some build output (StockItem) objects
|
||||
self.output_1 = StockItem.objects.create(
|
||||
part=self.assembly,
|
||||
quantity=5,
|
||||
quantity=3,
|
||||
is_building=True,
|
||||
build=self.build
|
||||
)
|
||||
|
||||
self.output_2 = StockItem.objects.create(
|
||||
part=self.assembly,
|
||||
quantity=5,
|
||||
quantity=7,
|
||||
is_building=True,
|
||||
build=self.build,
|
||||
)
|
||||
@ -82,10 +108,12 @@ class BuildTest(TestCase):
|
||||
|
||||
self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5000)
|
||||
|
||||
self.stock_3_1 = StockItem.objects.create(part=self.sub_part_3, quantity=1000)
|
||||
|
||||
def test_init(self):
|
||||
# Perform some basic tests before we start the ball rolling
|
||||
|
||||
self.assertEqual(StockItem.objects.count(), 5)
|
||||
self.assertEqual(StockItem.objects.count(), 6)
|
||||
|
||||
# Build is PENDING
|
||||
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
||||
@ -100,10 +128,10 @@ class BuildTest(TestCase):
|
||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
|
||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
||||
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 50)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 50)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 125)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 125)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 15)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 35)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 9)
|
||||
self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 21)
|
||||
|
||||
self.assertFalse(self.build.is_complete)
|
||||
|
||||
@ -144,84 +172,113 @@ class BuildTest(TestCase):
|
||||
quantity=99
|
||||
)
|
||||
|
||||
def allocate_stock(self, q11, q12, q21, output):
|
||||
# Assign stock to this build
|
||||
def allocate_stock(self, output, allocations):
|
||||
"""
|
||||
Allocate stock to this build, against a particular output
|
||||
|
||||
if q11 > 0:
|
||||
Args:
|
||||
output - StockItem object (or None)
|
||||
allocations - Map of {StockItem: quantity}
|
||||
"""
|
||||
|
||||
for item, quantity in allocations.items():
|
||||
BuildItem.objects.create(
|
||||
build=self.build,
|
||||
stock_item=self.stock_1_1,
|
||||
quantity=q11,
|
||||
stock_item=item,
|
||||
quantity=quantity,
|
||||
install_into=output
|
||||
)
|
||||
|
||||
if q12 > 0:
|
||||
BuildItem.objects.create(
|
||||
build=self.build,
|
||||
stock_item=self.stock_1_2,
|
||||
quantity=q12,
|
||||
install_into=output
|
||||
)
|
||||
|
||||
if q21 > 0:
|
||||
BuildItem.objects.create(
|
||||
build=self.build,
|
||||
stock_item=self.stock_2_1,
|
||||
quantity=q21,
|
||||
install_into=output,
|
||||
)
|
||||
|
||||
# Attempt to create another identical BuildItem
|
||||
b = BuildItem(
|
||||
build=self.build,
|
||||
stock_item=self.stock_2_1,
|
||||
quantity=q21
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
b.clean()
|
||||
|
||||
def test_partial_allocation(self):
|
||||
"""
|
||||
Partially allocate against output 1
|
||||
Test partial allocation of stock
|
||||
"""
|
||||
|
||||
self.allocate_stock(50, 50, 200, self.output_1)
|
||||
# Fully allocate tracked stock against build output 1
|
||||
self.allocate_stock(
|
||||
self.output_1,
|
||||
{
|
||||
self.stock_3_1: 6,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
||||
|
||||
# Partially allocate tracked stock against build output 2
|
||||
self.allocate_stock(
|
||||
self.output_2,
|
||||
{
|
||||
self.stock_3_1: 1,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(self.build.isFullyAllocated(self.output_2))
|
||||
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
|
||||
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1))
|
||||
|
||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_2))
|
||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
||||
|
||||
# Check that the part has been allocated
|
||||
self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 100)
|
||||
# Partially allocate untracked stock against build
|
||||
self.allocate_stock(
|
||||
None,
|
||||
{
|
||||
self.stock_1_1: 1,
|
||||
self.stock_2_1: 1
|
||||
}
|
||||
)
|
||||
|
||||
self.build.unallocateStock(output=self.output_1)
|
||||
self.assertEqual(BuildItem.objects.count(), 0)
|
||||
self.assertFalse(self.build.isFullyAllocated(None, verbose=True))
|
||||
|
||||
# Check that the part has been unallocated
|
||||
self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 0)
|
||||
unallocated = self.build.unallocatedParts(None)
|
||||
|
||||
self.assertEqual(len(unallocated), 2)
|
||||
|
||||
self.allocate_stock(
|
||||
None,
|
||||
{
|
||||
self.stock_1_2: 100,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(self.build.isFullyAllocated(None, verbose=True))
|
||||
|
||||
unallocated = self.build.unallocatedParts(None)
|
||||
|
||||
self.assertEqual(len(unallocated), 1)
|
||||
|
||||
self.build.unallocateUntracked()
|
||||
|
||||
unallocated = self.build.unallocatedParts(None)
|
||||
|
||||
self.assertEqual(len(unallocated), 2)
|
||||
|
||||
self.assertFalse(self.build.areUntrackedPartsFullyAllocated())
|
||||
|
||||
# Now we "fully" allocate the untracked untracked items
|
||||
self.allocate_stock(
|
||||
None,
|
||||
{
|
||||
self.stock_1_1: 50,
|
||||
self.stock_2_1: 50,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(self.build.areUntrackedPartsFullyAllocated())
|
||||
|
||||
def test_auto_allocate(self):
|
||||
"""
|
||||
Test auto-allocation functionality against the build outputs
|
||||
Test auto-allocation functionality against the build outputs.
|
||||
|
||||
Note: auto-allocations only work for un-tracked stock!
|
||||
"""
|
||||
|
||||
allocations = self.build.getAutoAllocations(self.output_1)
|
||||
allocations = self.build.getAutoAllocations()
|
||||
|
||||
self.assertEqual(len(allocations), 1)
|
||||
|
||||
self.build.autoAllocate(self.output_1)
|
||||
self.build.autoAllocate()
|
||||
self.assertEqual(BuildItem.objects.count(), 1)
|
||||
|
||||
# Check that one part has been fully allocated to the build output
|
||||
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1))
|
||||
# Check that one un-tracked part has been fully allocated to the build
|
||||
self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, None))
|
||||
|
||||
# But, the *other* build output has not been allocated against
|
||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))
|
||||
self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, None))
|
||||
|
||||
def test_cancel(self):
|
||||
"""
|
||||
@ -243,9 +300,33 @@ class BuildTest(TestCase):
|
||||
Test completion of a build output
|
||||
"""
|
||||
|
||||
self.allocate_stock(50, 50, 250, self.output_1)
|
||||
self.allocate_stock(50, 50, 250, self.output_2)
|
||||
# Allocate non-tracked parts
|
||||
self.allocate_stock(
|
||||
None,
|
||||
{
|
||||
self.stock_1_1: self.stock_1_1.quantity, # Allocate *all* stock from this item
|
||||
self.stock_1_2: 10,
|
||||
self.stock_2_1: 30
|
||||
}
|
||||
)
|
||||
|
||||
# Allocate tracked parts to output_1
|
||||
self.allocate_stock(
|
||||
self.output_1,
|
||||
{
|
||||
self.stock_3_1: 6
|
||||
}
|
||||
)
|
||||
|
||||
# Allocate tracked parts to output_2
|
||||
self.allocate_stock(
|
||||
self.output_2,
|
||||
{
|
||||
self.stock_3_1: 14
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(self.build.isFullyAllocated(None, verbose=True))
|
||||
self.assertTrue(self.build.isFullyAllocated(self.output_1))
|
||||
self.assertTrue(self.build.isFullyAllocated(self.output_2))
|
||||
|
||||
@ -265,19 +346,16 @@ class BuildTest(TestCase):
|
||||
self.assertEqual(BuildItem.objects.count(), 0)
|
||||
|
||||
# New stock items should have been created!
|
||||
self.assertEqual(StockItem.objects.count(), 4)
|
||||
|
||||
a = StockItem.objects.get(pk=self.stock_1_1.pk)
|
||||
self.assertEqual(StockItem.objects.count(), 7)
|
||||
|
||||
# This stock item has been depleted!
|
||||
with self.assertRaises(StockItem.DoesNotExist):
|
||||
StockItem.objects.get(pk=self.stock_1_2.pk)
|
||||
StockItem.objects.get(pk=self.stock_1_1.pk)
|
||||
|
||||
c = StockItem.objects.get(pk=self.stock_2_1.pk)
|
||||
# This stock item has *not* been depleted
|
||||
x = StockItem.objects.get(pk=self.stock_2_1.pk)
|
||||
|
||||
# Stock should have been subtracted from the original items
|
||||
self.assertEqual(a.quantity, 900)
|
||||
self.assertEqual(c.quantity, 4500)
|
||||
self.assertEqual(x.quantity, 4970)
|
||||
|
||||
# And 10 new stock items created for the build output
|
||||
outputs = StockItem.objects.filter(build=self.build)
|
||||
|
@ -15,7 +15,7 @@ from datetime import datetime, timedelta
|
||||
from .models import Build
|
||||
from stock.models import StockItem
|
||||
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||
|
||||
|
||||
class BuildTestSimple(TestCase):
|
||||
@ -335,6 +335,7 @@ class TestBuildViews(TestCase):
|
||||
'confirm_incomplete': 1,
|
||||
'location': 1,
|
||||
'output': self.output.pk,
|
||||
'stock_status': StockStatus.DAMAGED
|
||||
},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
@ -342,6 +343,7 @@ class TestBuildViews(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = json.loads(response.content)
|
||||
|
||||
self.assertTrue(data['form_valid'])
|
||||
|
||||
# Now the build should be able to be completed
|
||||
|
@ -21,7 +21,6 @@ build_detail_urls = [
|
||||
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
||||
|
||||
url(r'^children/', views.BuildDetail.as_view(template_name='build/build_children.html'), name='build-children'),
|
||||
url(r'^parts/', views.BuildDetail.as_view(template_name='build/parts.html'), name='build-parts'),
|
||||
url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'),
|
||||
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
|
||||
|
||||
|
@ -18,8 +18,8 @@ from stock.models import StockLocation, StockItem
|
||||
|
||||
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
|
||||
from InvenTree.views import InvenTreeRoleMixin
|
||||
from InvenTree.helpers import str2bool, extract_serial_numbers, normalize
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.helpers import str2bool, extract_serial_numbers, normalize, isNull
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||
|
||||
|
||||
class BuildIndex(InvenTreeRoleMixin, ListView):
|
||||
@ -98,16 +98,6 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
# Pointing to a particular build output?
|
||||
output = self.get_param('output')
|
||||
|
||||
if output:
|
||||
try:
|
||||
output = StockItem.objects.get(pk=output)
|
||||
initials['output'] = output
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
@ -119,18 +109,7 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
|
||||
build = self.get_object()
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
output_id = form['output'].value()
|
||||
|
||||
try:
|
||||
output = StockItem.objects.get(pk=output_id)
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
output = None
|
||||
|
||||
if output:
|
||||
context['output'] = output
|
||||
context['allocations'] = build.getAutoAllocations(output)
|
||||
context['allocations'] = build.getAutoAllocations()
|
||||
|
||||
context['build'] = build
|
||||
|
||||
@ -140,18 +119,11 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
if form['output'].value():
|
||||
# Hide the 'output' field
|
||||
form.fields['output'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
def validate(self, build, form, **kwargs):
|
||||
|
||||
output = form.cleaned_data.get('output', None)
|
||||
|
||||
if not output:
|
||||
form.add_error(None, _('Build output must be specified'))
|
||||
pass
|
||||
|
||||
def save(self, build, form, **kwargs):
|
||||
"""
|
||||
@ -159,9 +131,7 @@ class BuildAutoAllocate(AjaxUpdateView):
|
||||
perform auto-allocations
|
||||
"""
|
||||
|
||||
output = form.cleaned_data.get('output', None)
|
||||
|
||||
build.autoAllocate(output)
|
||||
build.autoAllocate()
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
@ -242,7 +212,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
||||
|
||||
# Calculate the required quantity
|
||||
quantity = max(0, build.remaining - build.incomplete_count)
|
||||
initials['quantity'] = quantity
|
||||
initials['output_quantity'] = quantity
|
||||
|
||||
return initials
|
||||
|
||||
@ -365,10 +335,16 @@ class BuildUnallocate(AjaxUpdateView):
|
||||
|
||||
output_id = request.POST.get('output_id', None)
|
||||
|
||||
try:
|
||||
output = StockItem.objects.get(pk=output_id)
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
output = None
|
||||
if output_id:
|
||||
|
||||
# If a "null" output is provided, we are trying to unallocate "untracked" stock
|
||||
if isNull(output_id):
|
||||
output = None
|
||||
else:
|
||||
try:
|
||||
output = StockItem.objects.get(pk=output_id)
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
output = None
|
||||
|
||||
part_id = request.POST.get('part_id', None)
|
||||
|
||||
@ -383,9 +359,19 @@ class BuildUnallocate(AjaxUpdateView):
|
||||
form.add_error('confirm', _('Confirm unallocation of build stock'))
|
||||
form.add_error(None, _('Check the confirmation box'))
|
||||
else:
|
||||
build.unallocateStock(output=output, part=part)
|
||||
|
||||
valid = True
|
||||
|
||||
# Unallocate the entire build
|
||||
if not output_id:
|
||||
build.unallocateAll()
|
||||
# Unallocate a single output
|
||||
elif output:
|
||||
build.unallocateOutput(output, part=part)
|
||||
# Unallocate "untracked" parts
|
||||
else:
|
||||
build.unallocateUntracked(part=part)
|
||||
|
||||
data = {
|
||||
'form_valid': valid,
|
||||
}
|
||||
@ -410,8 +396,8 @@ class BuildComplete(AjaxUpdateView):
|
||||
|
||||
def validate(self, build, form, **kwargs):
|
||||
|
||||
if not build.can_complete:
|
||||
form.add_error(None, _('Build order cannot be completed'))
|
||||
if build.incomplete_count > 0:
|
||||
form.add_error(None, _('Build order cannot be completed - incomplete outputs remain'))
|
||||
|
||||
def save(self, build, form, **kwargs):
|
||||
"""
|
||||
@ -431,7 +417,7 @@ class BuildOutputComplete(AjaxUpdateView):
|
||||
View to mark a particular build output as Complete.
|
||||
|
||||
- Notifies the user of which parts will be removed from stock.
|
||||
- Removes allocated items from stock
|
||||
- Assignes (tracked) allocated items from stock to the build output
|
||||
- Deletes pending BuildItem objects
|
||||
"""
|
||||
|
||||
@ -463,11 +449,25 @@ class BuildOutputComplete(AjaxUpdateView):
|
||||
return form
|
||||
|
||||
def validate(self, build, form, **kwargs):
|
||||
"""
|
||||
Custom validation steps for the BuildOutputComplete" form
|
||||
"""
|
||||
|
||||
data = form.cleaned_data
|
||||
|
||||
output = data.get('output', None)
|
||||
|
||||
stock_status = data.get('stock_status', StockStatus.OK)
|
||||
|
||||
# Any "invalid" stock status defaults to OK
|
||||
try:
|
||||
stock_status = int(stock_status)
|
||||
except (ValueError):
|
||||
stock_status = StockStatus.OK
|
||||
|
||||
if int(stock_status) not in StockStatus.keys():
|
||||
form.add_error('stock_status', _('Invalid stock status value selected'))
|
||||
|
||||
if output:
|
||||
|
||||
quantity = data.get('quantity', None)
|
||||
@ -559,12 +559,20 @@ class BuildOutputComplete(AjaxUpdateView):
|
||||
|
||||
location = data.get('location', None)
|
||||
output = data.get('output', None)
|
||||
stock_status = data.get('stock_status', StockStatus.OK)
|
||||
|
||||
# Any "invalid" stock status defaults to OK
|
||||
try:
|
||||
stock_status = int(stock_status)
|
||||
except (ValueError):
|
||||
stock_status = StockStatus.OK
|
||||
|
||||
# Complete the build output
|
||||
build.completeBuildOutput(
|
||||
output,
|
||||
self.request.user,
|
||||
location=location,
|
||||
status=stock_status,
|
||||
)
|
||||
|
||||
def get_data(self):
|
||||
@ -632,10 +640,12 @@ class BuildAllocate(InvenTreeRoleMixin, DetailView):
|
||||
|
||||
build = self.get_object()
|
||||
part = build.part
|
||||
bom_items = part.bom_items
|
||||
bom_items = build.bom_items
|
||||
|
||||
context['part'] = part
|
||||
context['bom_items'] = bom_items
|
||||
context['has_tracked_bom_items'] = build.has_tracked_bom_items()
|
||||
context['has_untracked_bom_items'] = build.has_untracked_bom_items()
|
||||
context['BuildStatus'] = BuildStatus
|
||||
|
||||
context['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
||||
|
@ -58,6 +58,13 @@ class InvenTreeSetting(models.Model):
|
||||
'description': _('String descriptor for the server instance'),
|
||||
},
|
||||
|
||||
'INVENTREE_INSTANCE_TITLE': {
|
||||
'name': _('Use instance name'),
|
||||
'description': _('Use the instance name in the title-bar'),
|
||||
'validator': bool,
|
||||
'default': False,
|
||||
},
|
||||
|
||||
'INVENTREE_COMPANY_NAME': {
|
||||
'name': _('Company name'),
|
||||
'description': _('Internal company name'),
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Company" %} - {{ company.name }}
|
||||
{% inventree_title %} | {% trans "Company" %} - {{ company.name }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Supplier List" %}
|
||||
{% inventree_title %} | {% trans "Supplier List" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -33,8 +33,7 @@
|
||||
</table>
|
||||
{% if part.supplier_parts.all|length > 0 %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
<p>There are {{ part.supplier_parts.all|length }} suppliers defined for this manufacturer part. If you delete it, the following supplier parts will also be deleted:
|
||||
</p>
|
||||
<p>{% blocktrans with count=part.supplier_parts.all|length %}There are {{count}} suppliers defined for this manufacturer part. If you delete it, the following supplier parts will also be deleted:{% endblocktrans %}</p>
|
||||
<ul class='list-group' style='margin-top:10px'>
|
||||
{% for spart in part.supplier_parts.all %}
|
||||
<li class='list-group-item'>{{ spart.supplier.name }} - {{ spart.SKU }}</li>
|
||||
|
@ -1,9 +1,10 @@
|
||||
{% extends "two_column.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Supplier Part" %}
|
||||
{% inventree_title %} | {% trans "Supplier Part" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
|
@ -85,6 +85,7 @@ email:
|
||||
port: 25
|
||||
username: ''
|
||||
password: ''
|
||||
sender: ''
|
||||
tls: False
|
||||
ssl: False
|
||||
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-15 10:07+0000\n"
|
||||
"POT-Creation-Date: 2021-04-14 11:13+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -2026,9 +2026,8 @@ msgid "Supplied Parts"
|
||||
msgstr ""
|
||||
|
||||
#: company/templates/company/navbar.html:23
|
||||
#: order/templates/order/receive_parts.html:14 part/api.py:40
|
||||
#: part/models.py:322 part/templates/part/cat_link.html:7
|
||||
#: part/templates/part/category.html:95
|
||||
#: order/templates/order/receive_parts.html:14 part/models.py:322
|
||||
#: part/templates/part/cat_link.html:7 part/templates/part/category.html:95
|
||||
#: part/templates/part/category_navbar.html:11
|
||||
#: part/templates/part/category_navbar.html:14
|
||||
#: part/templates/part/category_partlist.html:10
|
||||
@ -2047,8 +2046,8 @@ msgstr ""
|
||||
|
||||
#: company/templates/company/navbar.html:30
|
||||
#: company/templates/company/part_navbar.html:14
|
||||
#: part/templates/part/navbar.html:36 stock/api.py:51
|
||||
#: stock/templates/stock/loc_link.html:7 stock/templates/stock/location.html:29
|
||||
#: part/templates/part/navbar.html:36 stock/templates/stock/loc_link.html:7
|
||||
#: stock/templates/stock/location.html:29
|
||||
#: stock/templates/stock/stock_app_base.html:9
|
||||
#: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180
|
||||
#: templates/InvenTree/search.html:216
|
||||
@ -6068,14 +6067,6 @@ msgstr ""
|
||||
msgid "Assembled part"
|
||||
msgstr ""
|
||||
|
||||
#: templates/js/filters.js:167 templates/js/filters.js:397
|
||||
msgid "true"
|
||||
msgstr ""
|
||||
|
||||
#: templates/js/filters.js:171 templates/js/filters.js:398
|
||||
msgid "false"
|
||||
msgstr ""
|
||||
|
||||
#: templates/js/filters.js:193
|
||||
msgid "Select filter"
|
||||
msgstr ""
|
||||
@ -6408,18 +6399,6 @@ msgstr ""
|
||||
msgid "No stock items matching query"
|
||||
msgstr ""
|
||||
|
||||
#: templates/js/stock.js:357
|
||||
msgid "items"
|
||||
msgstr ""
|
||||
|
||||
#: templates/js/stock.js:449
|
||||
msgid "batches"
|
||||
msgstr ""
|
||||
|
||||
#: templates/js/stock.js:476
|
||||
msgid "locations"
|
||||
msgstr ""
|
||||
|
||||
#: templates/js/stock.js:478
|
||||
msgid "Undefined location"
|
||||
msgstr ""
|
||||
|
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7224
InvenTree/locale/fr/LC_MESSAGES/django.po
Normal file
7224
InvenTree/locale/fr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/it/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/it/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7224
InvenTree/locale/it/LC_MESSAGES/django.po
Normal file
7224
InvenTree/locale/it/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/ja/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/ja/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7223
InvenTree/locale/ja/LC_MESSAGES/django.po
Normal file
7223
InvenTree/locale/ja/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/pl/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/pl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7223
InvenTree/locale/pl/LC_MESSAGES/django.po
Normal file
7223
InvenTree/locale/pl/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/ru/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/ru/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7223
InvenTree/locale/ru/LC_MESSAGES/django.po
Normal file
7223
InvenTree/locale/ru/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/tr/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/tr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7288
InvenTree/locale/tr/LC_MESSAGES/django.po
Normal file
7288
InvenTree/locale/tr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/zh/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/zh/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7224
InvenTree/locale/zh/LC_MESSAGES/django.po
Normal file
7224
InvenTree/locale/zh/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@
|
||||
{% load status_codes %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Purchase Order" %}
|
||||
{% inventree_title %} | {% trans "Purchase Order" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
|
@ -7,8 +7,8 @@
|
||||
{% trans 'Mark this order as complete?' %}
|
||||
{% if not order.is_complete %}
|
||||
<div class='alert alert-warning alert-block'>
|
||||
{%trans 'This order has line items which have not been marked as received.
|
||||
Marking this order as complete will remove these line items.' %}
|
||||
{% trans 'This order has line items which have not been marked as received.' %}
|
||||
{% trans 'Marking this order as complete will remove these line items.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -54,7 +54,7 @@
|
||||
</select>
|
||||
</div>
|
||||
{% if not part.order_supplier %}
|
||||
<span class='help-inline'>{% trans "Select a supplier for" %} <i>{{ part.name }}</i></span>
|
||||
<span class='help-inline'>{% blocktrans with name=part.name %}Select a supplier for <i>{{name}}</i>{% endblocktrans %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
@ -42,7 +42,7 @@
|
||||
<button
|
||||
class='btn btn-default btn-create'
|
||||
id='new_po_{{ supplier.id }}'
|
||||
title='{% trans "Create new purchase order for {{ supplier.name }}" %}'
|
||||
title='{% blocktrans with name=supplier.name %}Create new purchase order for {{name}}{% endblocktrans %}'
|
||||
type='button'
|
||||
supplierid='{{ supplier.id }}'
|
||||
onclick='newPurchaseOrderFromOrderWizard()'>
|
||||
@ -65,7 +65,7 @@
|
||||
</select>
|
||||
</div>
|
||||
{% if not supplier.selected_purchase_order %}
|
||||
<span class='help-inline'>{% trans "Select a purchase order for" %} {{ supplier.name }}</span>
|
||||
<span class='help-inline'>{% blocktrans with name=supplier.name %}Select a purchase order for {{name}}{% endblocktrans %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Purchase Orders" %}
|
||||
{% inventree_title %} | {% trans "Purchase Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% trans "Receive outstanding parts for" %} <b>{{ order }}</b> - <i>{{ order.description }}</i>
|
||||
{% blocktrans with desc=order.description %}Receive outstanding parts for <b>{{order}}</b> - <i>{{desc}}</i>{% endblocktrans %}
|
||||
|
||||
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
||||
{% csrf_token %}
|
||||
|
@ -6,7 +6,7 @@
|
||||
{% load status_codes %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Sales Order" %}
|
||||
{% inventree_title %} | {% trans "Sales Order" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content %}
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Sales Orders" %}
|
||||
{% inventree_title %} | {% trans "Sales Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -60,28 +60,43 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
queryset = PartCategory.objects.all()
|
||||
serializer_class = part_serializers.CategorySerializer
|
||||
|
||||
def get_queryset(self):
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering:
|
||||
- Allow filtering by "null" parent to retrieve top-level part categories
|
||||
"""
|
||||
|
||||
cat_id = self.request.query_params.get('parent', None)
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
queryset = super().get_queryset()
|
||||
params = self.request.query_params
|
||||
|
||||
if cat_id is not None:
|
||||
cat_id = params.get('parent', None)
|
||||
|
||||
cascade = str2bool(params.get('cascade', False))
|
||||
|
||||
# Do not filter by category
|
||||
if cat_id is None:
|
||||
pass
|
||||
# Look for top-level categories
|
||||
elif isNull(cat_id):
|
||||
|
||||
# Look for top-level categories
|
||||
if isNull(cat_id):
|
||||
if not cascade:
|
||||
queryset = queryset.filter(parent=None)
|
||||
|
||||
else:
|
||||
try:
|
||||
cat_id = int(cat_id)
|
||||
queryset = queryset.filter(parent=cat_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
else:
|
||||
try:
|
||||
category = PartCategory.objects.get(pk=cat_id)
|
||||
|
||||
if cascade:
|
||||
parents = category.get_descendants(include_self=True)
|
||||
parent_ids = [p.id for p in parents]
|
||||
|
||||
queryset = queryset.filter(parent__in=parent_ids)
|
||||
else:
|
||||
queryset = queryset.filter(parent=category)
|
||||
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
|
@ -1163,7 +1163,16 @@ class Part(MPTTModel):
|
||||
Return the total amount of this part allocated to build orders
|
||||
"""
|
||||
|
||||
query = self.build_order_allocations().aggregate(total=Coalesce(Sum('quantity'), 0))
|
||||
query = self.build_order_allocations().aggregate(
|
||||
total=Coalesce(
|
||||
Sum(
|
||||
'quantity',
|
||||
output_field=models.DecimalField()
|
||||
),
|
||||
0,
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
return query['total']
|
||||
|
||||
@ -1179,7 +1188,16 @@ class Part(MPTTModel):
|
||||
Return the tutal quantity of this part allocated to sales orders
|
||||
"""
|
||||
|
||||
query = self.sales_order_allocations().aggregate(total=Coalesce(Sum('quantity'), 0))
|
||||
query = self.sales_order_allocations().aggregate(
|
||||
total=Coalesce(
|
||||
Sum(
|
||||
'quantity',
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
0,
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
return query['total']
|
||||
|
||||
@ -1189,10 +1207,12 @@ class Part(MPTTModel):
|
||||
against both build orders and sales orders.
|
||||
"""
|
||||
|
||||
return sum([
|
||||
self.build_order_allocation_count(),
|
||||
self.sales_order_allocation_count(),
|
||||
])
|
||||
return sum(
|
||||
[
|
||||
self.build_order_allocation_count(),
|
||||
self.sales_order_allocation_count(),
|
||||
],
|
||||
)
|
||||
|
||||
def stock_entries(self, include_variants=True, in_stock=None):
|
||||
""" Return all stock entries for this Part.
|
||||
|
@ -4,6 +4,7 @@ JSON serializers for Part app
|
||||
import imghdr
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||
@ -208,7 +209,8 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
queryset = queryset.annotate(
|
||||
in_stock=Coalesce(
|
||||
SubquerySum('stock_items__quantity', filter=StockItem.IN_STOCK_FILTER),
|
||||
Decimal(0)
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
)
|
||||
|
||||
@ -227,6 +229,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
building=Coalesce(
|
||||
SubquerySum('builds__quantity', filter=build_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
@ -240,9 +243,11 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
ordering=Coalesce(
|
||||
SubquerySum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
) - Coalesce(
|
||||
SubquerySum('supplier_parts__purchase_order_line_items__received', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
@ -251,6 +256,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
suppliers=Coalesce(
|
||||
SubqueryCount('supplier_parts'),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -2,6 +2,10 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='parts' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
@ -100,14 +104,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if category and category.children.all|length > 0 %}
|
||||
{% include "part/subcategories.html" with children=category.children.all collapse_id="categories" %}
|
||||
{% elif children|length > 0 %}
|
||||
{% include "part/subcategories.html" with children=children collapse_id="categories" %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% block category_content %}
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
||||
@ -150,6 +150,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block category_tables %}
|
||||
{% endblock category_tables %}
|
||||
@ -162,24 +164,10 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if category %}
|
||||
enableNavbar({
|
||||
label: 'category',
|
||||
toggleId: '#category-menu-toggle',
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
if (inventreeLoadInt("show-part-cats") == 1) {
|
||||
$("#collapse-item-categories").collapse('show');
|
||||
}
|
||||
|
||||
$("#collapse-item-categories").on('shown.bs.collapse', function() {
|
||||
inventreeSave('show-part-cats', 1);
|
||||
});
|
||||
|
||||
$("#collapse-item-categories").on('hidden.bs.collapse', function() {
|
||||
inventreeDel('show-part-cats');
|
||||
});
|
||||
|
||||
$("#cat-create").click(function() {
|
||||
launchModalForm(
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% trans 'Are you sure you want to delete category' %} <strong>{{ category.name }}</strong>?
|
||||
|
||||
{% if category.children.all|length > 0 %}
|
||||
<p>{% trans 'This category contains' %} {{ category.children.all|length }} {% trans 'child categories' %}.<br>
|
||||
<p>{% blocktrans with count=category.children.all|length%}This category contains {{count}} child categories{% endblocktrans %}.<br>
|
||||
{% trans 'If this category is deleted, these child categories will be moved to the' %}
|
||||
{% if category.parent %}
|
||||
<strong>{{ category.parent.name }}</strong> {% trans 'category' %}.
|
||||
@ -22,9 +22,9 @@
|
||||
{% endif %}
|
||||
|
||||
{% if category.parts.all|length > 0 %}
|
||||
<p>{% trans 'This category contains' %} {{ category.parts.all|length }} {% trans 'parts' %}.<br>
|
||||
<p>{% blocktrans with count=category.parts.all|length %}This category contains {{count}} parts{% endblocktrans %}.<br>
|
||||
{% if category.parent %}
|
||||
{% trans 'If this category is deleted, these parts will be moved to the parent category' %} {{ category.parent.pathstring }}
|
||||
{% blocktrans with path=category.parent.pathstring %}If this category is deleted, these parts will be moved to the parent category {{path}}{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans 'If this category is deleted, these parts will be moved to the top-level category Teile' %}
|
||||
{% endif %}
|
||||
|
@ -8,17 +8,34 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "subcategories" %}active{% endif %}' title='{% trans "Subcategories" %}'>
|
||||
{% if category %}
|
||||
<a href='{% url "category-subcategory" category.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "category-index-subcategory" %}'>
|
||||
{% endif %}
|
||||
<span class='fas fa-sitemap'></span>
|
||||
{% trans "Subcategories" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Parts" %}'>
|
||||
{% if category %}
|
||||
<a href='{% url "category-detail" category.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "part-index" %}'>
|
||||
{% endif %}
|
||||
<span class='fas fa-shapes'></span>
|
||||
{% trans "Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if category %}
|
||||
<li class='list-group-item {% if tab == "parameters" %}active{% endif %}' title='{% trans "Parameters" %}'>
|
||||
<a href='{% url "category-parametric" category.id %}'>
|
||||
<span class='fas fa-tasks'></span>
|
||||
{% trans "Parameters" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
@ -7,7 +7,7 @@
|
||||
|
||||
<div class='alert alert-info alert-block'>
|
||||
<strong>{% trans 'Duplicate Part' %}</strong><br>
|
||||
{% trans 'Make a copy of part' %} '{{ part.full_name }}'.
|
||||
{% blocktrans with full_name=part.full_name %}Make a copy of part '{{full_name}}'.{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
{% if matches %}
|
||||
|
@ -13,7 +13,8 @@
|
||||
<ul class='list-group'>
|
||||
{% for match in matches %}
|
||||
<li class='list-group-item list-group-item-condensed'>
|
||||
{{ match.part.full_name }} - <i>{{ match.part.description }}</i> ({% decimal match.ratio %}% {% trans "match" %})
|
||||
{% decimal match.ratio as match_per %}
|
||||
{% blocktrans with full_name=match.part.full_name desc=match.part.description %}{{full_name}} - <i>{{desc}}</i> ({{match_per}}% match){% endblocktrans %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -1,14 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
{% if part %}
|
||||
InvenTree | {% trans "Part" %} - {{ part.full_name }}
|
||||
{% inventree_title %} | {% trans "Part" %} - {{ part.full_name }}
|
||||
{% elif category %}
|
||||
InvenTree | {% trans "Part Category" %} - {{ category }}
|
||||
{% inventree_title %} | {% trans "Part Category" %} - {{ category }}
|
||||
{% else %}
|
||||
InvenTree | {% trans "Part List" %}
|
||||
{% inventree_title %} | {% trans "Part List" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -14,7 +14,8 @@
|
||||
|
||||
{% if part.variant_of %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% trans "This part is a variant of" %} <strong><a href="{% url 'part-variants' part.variant_of.id %}">{{ part.variant_of.full_name }}</a></strong>
|
||||
{% object_link 'part-variants' part.variant_of.id part.variant_of.full_name as link %}
|
||||
{% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -5,8 +5,7 @@
|
||||
{% block pre_form_content %}
|
||||
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% trans 'Pricing information for:' %}<br>
|
||||
{{ part }}.
|
||||
{% blocktrans %}Pricing information for:<br>{{part}}.{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
<h4>{% trans 'Quantity' %}</h4>
|
||||
|
@ -4,12 +4,12 @@
|
||||
{% block pre_form_content %}
|
||||
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you want to delete part" %} '<b>{{ part.full_name }}</b>'?
|
||||
{% blocktrans with full_name=part.full_name %}Are you sure you want to delete part '<b>{{full_name}}</b>'?{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
{% if part.used_in_count %}
|
||||
<hr>
|
||||
<p>{% trans "This part is used in BOMs for" %} {{ part.used_in_count }} {% trans "other parts. If you delete this part, the BOMs for the following parts will be updated" %}:
|
||||
<p>{% blocktrans with count=part.used_in_count %}This part is used in BOMs for {{count}} other parts. If you delete this part, the BOMs for the following parts will be updated{% endblocktrans %}:
|
||||
<ul class="list-group">
|
||||
{% for child in part.used_in.all %}
|
||||
<li class='list-group-item'>{{ child.part.full_name }} - {{ child.part.description }}</li>
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
{% if part.stock_items.all|length > 0 %}
|
||||
<hr>
|
||||
<p>{% trans "There are" %} {{ part.stock_items.all|length }} {% trans "stock entries defined for this part. If you delete this part, the following stock entries will also be deleted" %}:
|
||||
<p>{% blocktrans with count=part.stock_items.all|length %}There are {{count}} stock entries defined for this part. If you delete this part, the following stock entries will also be deleted:{% endblocktrans %}
|
||||
<ul class='list-group'>
|
||||
{% for stock in part.stock_items.all %}
|
||||
<li class='list-group-item'>{{ stock }}</li>
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
{% if part.manufacturer_parts.all|length > 0 %}
|
||||
<hr>
|
||||
<p>{% trans "There are" %} {{ part.manufacturer_parts.all|length }} {% trans "manufacturers defined for this part. If you delete this part, the following manufacturer parts will also be deleted" %}:
|
||||
<p>{% blocktrans with count=part.manufacturer_parts.all|length %}There are {{count}} manufacturers defined for this part. If you delete this part, the following manufacturer parts will also be deleted:{% endblocktrans %}
|
||||
<ul class='list-group'>
|
||||
{% for spart in part.manufacturer_parts.all %}
|
||||
<li class='list-group-item'>{{ spart.manufacturer.name }} - {{ spart.MPN }}</li>
|
||||
@ -41,7 +41,7 @@
|
||||
|
||||
{% if part.supplier_parts.all|length > 0 %}
|
||||
<hr>
|
||||
<p>{% trans "There are" %} {{ part.supplier_parts.all|length }} {% trans "suppliers defined for this part. If you delete this part, the following supplier parts will also be deleted" %}:
|
||||
<p>{% blocktrans with count=part.supplier_parts.all|length %}There are {{count}} suppliers defined for this part. If you delete this part, the following supplier parts will also be deleted:{% endblocktrans %}
|
||||
<ul class='list-group'>
|
||||
{% for spart in part.supplier_parts.all %}
|
||||
<li class='list-group-item'>{{ spart.supplier.name }} - {{ spart.SKU }}</li>
|
||||
@ -52,7 +52,7 @@
|
||||
|
||||
{% if part.serials.all|length > 0 %}
|
||||
<hr>
|
||||
<p>{% trans "There are" %} {{ part.serials.all|length }} {% trans "unique parts tracked for" %} '{{ part.full_name }}'. {% trans "Deleting this part will permanently remove this tracking information" %}.</p>
|
||||
<p>{% blocktrans with count=part.serials.all|length full_name=part.full_name %}There are {{count}} unique parts tracked for '{{full_name}}'. Deleting this part will permanently remove this tracking information.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -13,7 +13,7 @@
|
||||
{% block details %}
|
||||
{% if part.is_template %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% trans 'Showing stock for all variants of' %} <i>{{ part.full_name }}</i>
|
||||
{% blocktrans with full_name=part.full_name%}Showing stock for all variants of <i>{{full_name}}</i>{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
{% extends "collapse.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block collapse_title %}
|
||||
{{ children | length }} {% trans 'Child Categories' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block collapse_content %}
|
||||
<ul class="list-group">
|
||||
{% for child in children %}
|
||||
<li class="list-group-item">
|
||||
<strong><a href="{% url 'category-detail' child.id %}">{{ child.name }}</a></strong>
|
||||
{% if child.description %}
|
||||
<em> - {{ child.description }}</em>
|
||||
{% endif %}
|
||||
{% if child.partcount > 0 %}
|
||||
<span class='badge'>{{ child.partcount }} {% trans 'Part' %}{% if child.partcount > 1 %}s{% endif %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
51
InvenTree/part/templates/part/subcategory.html
Normal file
51
InvenTree/part/templates/part/subcategory.html
Normal file
@ -0,0 +1,51 @@
|
||||
{% extends "part/category.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
{% load static %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='subcategories' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block category_content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Subcategories" %}</h4>
|
||||
</div>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
|
||||
<div class='filter-list' id='filter-list-category'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='subcategory-table' data-toolbar='#button-toolbar'></table>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'category',
|
||||
toggleId: '#category-menu-toggle',
|
||||
});
|
||||
|
||||
loadPartCategoryTable($('#subcategory-table'), {
|
||||
params: {
|
||||
{% if category %}
|
||||
parent: {{ category.pk }}
|
||||
{% else %}
|
||||
parent: 'null'
|
||||
{% endif %}
|
||||
}
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -7,7 +7,7 @@
|
||||
|
||||
<div class='alert alert-info alert-block'>
|
||||
<b>{% trans "Create new part variant" %}</b><br>
|
||||
{% trans "Create a new variant of template" %} <i>'{{ part.full_name }}'</i>.
|
||||
{% blocktrans with full_name=part.full_name %}Create a new variant of template <i>'{{full_name}}'</i>.{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -4,6 +4,9 @@ over and above the built-in Django tags.
|
||||
import os
|
||||
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.templatetags.static import StaticNode
|
||||
from InvenTree import version, settings
|
||||
|
||||
import InvenTree.helpers
|
||||
@ -71,6 +74,12 @@ def inventree_instance_name(*args, **kwargs):
|
||||
return version.inventreeInstanceName()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_title(*args, **kwargs):
|
||||
""" Return the title for the current instance - respecting the settings """
|
||||
return version.inventreeInstanceTitle()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_version(*args, **kwargs):
|
||||
""" Return InvenTree version string """
|
||||
@ -164,3 +173,39 @@ def authorized_owners(group):
|
||||
pass
|
||||
|
||||
return owners
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def object_link(url_name, pk, ref):
|
||||
""" Return highlighted link to object """
|
||||
|
||||
ref_url = reverse(url_name, kwargs={'pk': pk})
|
||||
return mark_safe('<b><a href="{}">{}</a></b>'.format(ref_url, ref))
|
||||
|
||||
|
||||
class I18nStaticNode(StaticNode):
|
||||
"""
|
||||
custom StaticNode
|
||||
replaces a variable named *lng* in the path with the current language
|
||||
"""
|
||||
def render(self, context):
|
||||
self.path.var = self.path.var.format(lng=context.request.LANGUAGE_CODE)
|
||||
ret = super().render(context)
|
||||
return ret
|
||||
|
||||
|
||||
@register.tag('i18n_static')
|
||||
def do_i18n_static(parser, token):
|
||||
"""
|
||||
Overrides normal static, adds language - lookup for prerenderd files #1485
|
||||
|
||||
usage (like static):
|
||||
{% i18n_static path [as varname] %}
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
loc_name = settings.STATICFILES_I18_PREFIX
|
||||
|
||||
# change path to called ressource
|
||||
bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'"
|
||||
token.contents = ' '.join(bits)
|
||||
return I18nStaticNode.handle_token(parser, token)
|
||||
|
@ -37,12 +37,54 @@ class PartAPITest(InvenTreeAPITestCase):
|
||||
super().setUp()
|
||||
|
||||
def test_get_categories(self):
|
||||
""" Test that we can retrieve list of part categories """
|
||||
"""
|
||||
Test that we can retrieve list of part categories,
|
||||
with various filtering options.
|
||||
"""
|
||||
|
||||
url = reverse('api-part-category-list')
|
||||
|
||||
# Request *all* part categories
|
||||
response = self.client.get(url, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 8)
|
||||
|
||||
# Request top-level part categories only
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'parent': 'null',
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
||||
# Children of PartCategory<1>, cascade
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'parent': 1,
|
||||
'cascade': 'true',
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 5)
|
||||
|
||||
# Children of PartCategory<1>, do not cascade
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'parent': 1,
|
||||
'cascade': 'false',
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
def test_add_categories(self):
|
||||
""" Check that we can add categories """
|
||||
data = {
|
||||
|
@ -88,14 +88,26 @@ category_parameter_urls = [
|
||||
url(r'^(?P<pid>\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'),
|
||||
]
|
||||
|
||||
part_category_urls = [
|
||||
url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
category_urls = [
|
||||
|
||||
url(r'^parameters/', include(category_parameter_urls)),
|
||||
# Create a new category
|
||||
url(r'^new/', views.CategoryCreate.as_view(), name='category-create'),
|
||||
|
||||
url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
# Top level subcategory display
|
||||
url(r'^subcategory/', views.PartIndex.as_view(template_name='part/subcategory.html'), name='category-index-subcategory'),
|
||||
|
||||
# Category detail views
|
||||
url(r'(?P<pk>\d+)/', include([
|
||||
url(r'^edit/', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
url(r'^parameters/', include(category_parameter_urls)),
|
||||
|
||||
url(r'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'),
|
||||
url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||
|
||||
# Anything else
|
||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
]))
|
||||
]
|
||||
|
||||
part_bom_urls = [
|
||||
@ -106,9 +118,6 @@ part_bom_urls = [
|
||||
# URL list for part web interface
|
||||
part_urls = [
|
||||
|
||||
# Create a new category
|
||||
url(r'^category/new/?', views.CategoryCreate.as_view(), name='category-create'),
|
||||
|
||||
# Create a new part
|
||||
url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
|
||||
|
||||
@ -125,7 +134,7 @@ part_urls = [
|
||||
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
||||
|
||||
# Part category
|
||||
url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),
|
||||
url(r'^category/', include(category_urls)),
|
||||
|
||||
# Part related
|
||||
url(r'^related-parts/', include(part_related_urls)),
|
||||
|
@ -281,28 +281,46 @@ class StockLocationList(generics.ListCreateAPIView):
|
||||
queryset = StockLocation.objects.all()
|
||||
serializer_class = LocationSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering:
|
||||
- Allow filtering by "null" parent to retrieve top-level stock locations
|
||||
"""
|
||||
|
||||
queryset = super().get_queryset()
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
loc_id = self.request.query_params.get('parent', None)
|
||||
params = self.request.query_params
|
||||
|
||||
if loc_id is not None:
|
||||
loc_id = params.get('parent', None)
|
||||
|
||||
cascade = str2bool(params.get('cascade', False))
|
||||
|
||||
# Look for top-level locations
|
||||
if isNull(loc_id):
|
||||
# Do not filter by location
|
||||
if loc_id is None:
|
||||
pass
|
||||
# Look for top-level locations
|
||||
elif isNull(loc_id):
|
||||
|
||||
# If we allow "cascade" at the top-level, this essentially means *all* locations
|
||||
if not cascade:
|
||||
queryset = queryset.filter(parent=None)
|
||||
|
||||
else:
|
||||
try:
|
||||
loc_id = int(loc_id)
|
||||
queryset = queryset.filter(parent=loc_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
else:
|
||||
|
||||
try:
|
||||
location = StockLocation.objects.get(pk=loc_id)
|
||||
|
||||
# All sub-locations to be returned too?
|
||||
if cascade:
|
||||
parents = location.get_descendants(include_self=True)
|
||||
parent_ids = [p.id for p in parents]
|
||||
queryset = queryset.filter(parent__in=parent_ids)
|
||||
|
||||
else:
|
||||
queryset = queryset.filter(parent=location)
|
||||
|
||||
except (ValueError, StockLocation.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
@ -320,6 +338,11 @@ class StockLocationList(generics.ListCreateAPIView):
|
||||
'description',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
'name',
|
||||
'items',
|
||||
]
|
||||
|
||||
|
||||
class StockList(generics.ListCreateAPIView):
|
||||
""" API endpoint for list view of Stock objects
|
||||
|
@ -634,6 +634,7 @@ class StockItem(MPTTModel):
|
||||
|
||||
self.customer = None
|
||||
self.location = location
|
||||
self.sales_order = None
|
||||
|
||||
self.save()
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
{% inventree_title %} | {% trans "Stock Item" %} - {{ item }}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidenav %}
|
||||
@ -48,13 +48,17 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
|
||||
{% for allocation in item.sales_order_allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item is allocated to Sales Order" %} <a href="{% url 'so-detail' allocation.line.order.id %}"><b>#{{ allocation.line.order }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %})
|
||||
{% object_link 'so-detail' allocation.line.order.id allocation.line.order as link %}
|
||||
{% decimal allocation.quantity as qty %}
|
||||
{% blocktrans %}This stock item is allocated to Sales Order {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for allocation in item.allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item is allocated to Build" %} <a href="{% url 'build-detail' allocation.build.id %}"><b>#{{ allocation.build }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %})
|
||||
{% object_link 'build-detail' allocation.build.id allocation.build %}
|
||||
{% decimal allocation.quantity as qty %}
|
||||
{% blocktrans %}This stock item is allocated to Build {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@ -331,7 +335,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
<td><a href="{{ item.link }}">{{ item.link }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.supplier_part %}
|
||||
{% if item.supplier_part.manufacturer_part %}
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Manufacturer" %}</td>
|
||||
@ -342,6 +346,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
<td>{% trans "Manufacturer Part" %}</td>
|
||||
<td><a href="{% url 'manufacturer-part-detail' item.supplier_part.manufacturer_part.id %}">{{ item.supplier_part.manufacturer_part.MPN }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.supplier_part %}
|
||||
<tr>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
@ -360,9 +366,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
<td>
|
||||
{{ item.expiry_date }}
|
||||
{% if item.is_expired %}
|
||||
<span title='{% trans "This StockItem expired on" %} {{ item.expiry_date }}' class='label label-red'>{% trans "Expired" %}</span>
|
||||
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='label label-red'>{% trans "Expired" %}</span>
|
||||
{% elif item.is_stale %}
|
||||
<span title='{% trans "This StockItem expires on" %} {{ item.expiry_date }}' class='label label-yellow'>{% trans "Stale" %}</span>
|
||||
<span title='{% blocktrans %}This StockItem expires on {{ item.expiry_date }}{% endblocktrans %}' class='label label-yellow'>{% trans "Stale" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -30,7 +30,7 @@
|
||||
loadStockTable($("#stock-table"), {
|
||||
params: {
|
||||
location_detail: true,
|
||||
part_details: true,
|
||||
part_detail: false,
|
||||
ancestor: {{ item.id }},
|
||||
},
|
||||
name: 'item-childs',
|
||||
|
@ -8,7 +8,8 @@
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans "Are you sure you want to delete this stock item?" %}
|
||||
<br>
|
||||
This will remove <b>{% decimal item.quantity %}</b> units of <b>{{ item.part.full_name }}</b> from stock.
|
||||
{% decimal item.quantity as qty %}
|
||||
{% blocktrans with full_name=item.part.full_name %}This will remove <b>{{qty}}</b> units of <b>{{full_name}}</b> from stock.{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -2,8 +2,15 @@
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "stock/location_navbar.html" with tab="stock" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
|
||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||
{% if owner_control.value == "True" %}
|
||||
{% authorized_owners location.owner as owners %}
|
||||
@ -120,36 +127,29 @@
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if location and location.children.all|length > 0 %}
|
||||
{% include 'stock/location_list.html' with children=location.children.all collapse_id="locations" %}
|
||||
{% elif locations|length > 0 %}
|
||||
{% include 'stock/location_list.html' with children=locations collapse_id="locations" %}
|
||||
{% endif %}
|
||||
{% block location_content %}
|
||||
|
||||
<hr>
|
||||
|
||||
{% include "stock_table.html" %}
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Stock Items" %}</h4>
|
||||
</div>
|
||||
{% include "stock_table.html" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_load %}
|
||||
{{ block.super }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
if (inventreeLoadInt("show-part-locs") == 1) {
|
||||
$("#collapse-item-locations").collapse('show');
|
||||
}
|
||||
|
||||
$("#collapse-item-locations").on('shown.bs.collapse', function() {
|
||||
inventreeSave('show-part-locs', 1);
|
||||
});
|
||||
|
||||
$("#collapse-item-locations").on('hidden.bs.collapse', function() {
|
||||
inventreeDel('show-part-locs');
|
||||
enableNavbar({
|
||||
label: 'location',
|
||||
toggleId: '#location-menu-toggle'
|
||||
});
|
||||
|
||||
{% if location %}
|
||||
@ -261,7 +261,7 @@
|
||||
],
|
||||
params: {
|
||||
{% if location %}
|
||||
location: {{ location.id }},
|
||||
location: {{ location.pk }},
|
||||
{% endif %}
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
|
@ -1,24 +0,0 @@
|
||||
{% extends "collapse.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if roles.stock_location.view or roles.stock.view %}
|
||||
{% block collapse_title %}
|
||||
{% trans 'Sub-Locations' %}<span class='badge'>{{ children|length }}</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block collapse_content %}
|
||||
<ul class="list-group">
|
||||
{% for child in children %}
|
||||
<li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a> - <i>{{ child.description }}</i>
|
||||
{% if child.item_count > 0 %}
|
||||
<!-- span class='badge'>{{ child.item_count }} Item{% if child.item_count > 1 %}s{% endif %}</span> -->
|
||||
<span class='badge'>
|
||||
{% comment %}Translators: pluralize with counter{% endcomment %}
|
||||
{% blocktrans count counter=child.item_count %}{{ counter }} Item{% plural %}{{ counter }} Items{% endblocktrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% endif %}
|
33
InvenTree/stock/templates/stock/location_navbar.html
Normal file
33
InvenTree/stock/templates/stock/location_navbar.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% load i18n %}
|
||||
|
||||
<ul class='list-group'>
|
||||
|
||||
<li class='list-group-item'>
|
||||
<a href='#' id='location-menu-toggle'>
|
||||
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "sublocations" %}active{% endif %}' title='{% trans "Sublocations" %}'>
|
||||
{% if location %}
|
||||
<a href='{% url "stock-location-sublocation" location.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "stock-sublocations" %}'>
|
||||
{% endif %}
|
||||
<span class='fas fa-sitemap'></span>
|
||||
{% trans "Sublocations" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'>
|
||||
{% if location %}
|
||||
<a href='{% url "stock-location-detail" location.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "stock-index" %}'>
|
||||
{% endif %}
|
||||
<span class='fas fa-boxes'></span>
|
||||
{% trans "Stock Items" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
@ -1,12 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
{% if location %}
|
||||
InvenTree | {% trans "Stock Location" %} - {{ location }}
|
||||
{% inventree_title %} | {% trans "Stock Location" %} - {{ location }}
|
||||
{% else %}
|
||||
InvenTree | {% trans "Stock" %}
|
||||
{% inventree_title %} | {% trans "Stock" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
<div class='alert alert-block alert-info'>
|
||||
<b>{% trans "Convert Stock Item" %}</b><br>
|
||||
{% trans "This stock item is current an instance of " %}<i>{{ item.part }}</i><br>
|
||||
{% blocktrans with part=item.part %}This stock item is current an instance of <i>{{part}}</i>{% endblocktrans %}<br>
|
||||
{% trans "It can be converted to one of the part variants listed below." %}
|
||||
</div>
|
||||
|
||||
|
74
InvenTree/stock/templates/stock/sublocation.html
Normal file
74
InvenTree/stock/templates/stock/sublocation.html
Normal file
@ -0,0 +1,74 @@
|
||||
{% extends "stock/location.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "stock/location_navbar.html" with tab="sublocations" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block location_content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Sublocations" %}</h4>
|
||||
</div>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
<!-- Printing actions menu -->
|
||||
<div class='btn-group'>
|
||||
<button id='location-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'>
|
||||
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
<li><a href='#' id='multi-location-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-location'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='sublocation-table'></table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadStockLocationTable($('#sublocation-table'), {
|
||||
params: {
|
||||
{% if location %}
|
||||
parent: {{ location.pk }},
|
||||
{% else %}
|
||||
parent: 'null',
|
||||
{% endif %}
|
||||
}
|
||||
});
|
||||
|
||||
linkButtonsToSelection(
|
||||
$('#sublocation-table'),
|
||||
[
|
||||
'#location-print-options',
|
||||
]
|
||||
);
|
||||
|
||||
$('#multi-location-print-label').click(function() {
|
||||
|
||||
var selections = $('#sublocation-table').bootstrapTable('getSelections');
|
||||
|
||||
var locations = [];
|
||||
|
||||
selections.forEach(function(loc) {
|
||||
locations.push(loc.pk);
|
||||
});
|
||||
|
||||
printStockLocationLabels(locations);
|
||||
})
|
||||
|
||||
{% endblock %}
|
@ -3,7 +3,7 @@
|
||||
{% block pre_form_content %}
|
||||
|
||||
<div class='alert alert-danger alert-block'>
|
||||
Are you sure you want to delete this stock tracking entry?
|
||||
{% trans "Are you sure you want to delete this stock tracking entry?" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -6,14 +6,21 @@ from django.conf.urls import url, include
|
||||
|
||||
from . import views
|
||||
|
||||
# URL list for web interface
|
||||
stock_location_detail_urls = [
|
||||
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
|
||||
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'),
|
||||
url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'),
|
||||
location_urls = [
|
||||
|
||||
url(r'^new/', views.StockLocationCreate.as_view(), name='stock-location-create'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
|
||||
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'),
|
||||
url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'),
|
||||
|
||||
url(r'sublocation/', views.StockLocationDetail.as_view(template_name='stock/sublocation.html'), name='stock-location-sublocation'),
|
||||
|
||||
# Anything else
|
||||
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
|
||||
])),
|
||||
|
||||
# Anything else
|
||||
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
|
||||
]
|
||||
|
||||
stock_item_detail_urls = [
|
||||
@ -49,9 +56,7 @@ stock_tracking_urls = [
|
||||
|
||||
stock_urls = [
|
||||
# Stock location
|
||||
url(r'^location/(?P<pk>\d+)/', include(stock_location_detail_urls)),
|
||||
|
||||
url(r'^location/new/', views.StockLocationCreate.as_view(), name='stock-location-create'),
|
||||
url(r'^location/', include(location_urls)),
|
||||
|
||||
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
|
||||
|
||||
@ -81,5 +86,7 @@ stock_urls = [
|
||||
# Individual stock items
|
||||
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
||||
|
||||
url(r'^sublocations/', views.StockIndex.as_view(template_name='stock/sublocation.html'), name='stock-sublocations'),
|
||||
|
||||
url(r'^.*$', views.StockIndex.as_view(), name='stock-index'),
|
||||
]
|
||||
|
@ -2,12 +2,13 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Index" %}
|
||||
{% inventree_title %} | {% trans "Index" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>InvenTree</h3>
|
||||
<h3>{% inventree_title %} </h3>
|
||||
<hr>
|
||||
|
||||
<div class='col-sm-3' id='item-panel'>
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Search Results" %}
|
||||
{% inventree_title %} | {% trans "Search Results" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -16,6 +16,7 @@
|
||||
{% include "InvenTree/settings/header.html" %}
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE_TITLE" icon="fa-info-circle" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Settings" %}
|
||||
{% inventree_title %} | {% trans "Settings" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -60,7 +60,7 @@
|
||||
|
||||
<title>
|
||||
{% block page_title %}
|
||||
InvenTree
|
||||
{% inventree_title %}
|
||||
{% endblock %}
|
||||
</title>
|
||||
</head>
|
||||
@ -143,20 +143,21 @@ InvenTree
|
||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
|
||||
|
||||
<script type='text/javascript' src="{% url 'barcode.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'bom.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'company.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'part.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'modals.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'label.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'report.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'stock.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'build.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'order.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'calendar.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'tables.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'table_filters.js' %}"></script>
|
||||
<script type='text/javascript' src="{% url 'filters.js' %}"></script>
|
||||
<!-- translated -->
|
||||
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'company.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'part.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'modals.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'label.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'report.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'stock.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'build.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'calendar.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'table_filters.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'filters.js' %}"></script>
|
||||
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
|
||||
|
@ -268,6 +268,10 @@ function loadBomTable(table, options) {
|
||||
field: 'optional',
|
||||
title: '{% trans "Optional" %}',
|
||||
searchable: false,
|
||||
formatter: function(value) {
|
||||
if (value == '1') return '{% trans "true" %}';
|
||||
if (value == '0') return '{% trans "false" %}';
|
||||
}
|
||||
});
|
||||
|
||||
cols.push({
|
||||
|
@ -32,12 +32,17 @@ function newBuildOrder(options={}) {
|
||||
}
|
||||
|
||||
|
||||
function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
||||
/* Generate action buttons for a build output.
|
||||
*/
|
||||
|
||||
var buildId = buildInfo.pk;
|
||||
var outputId = output.pk;
|
||||
|
||||
if (output) {
|
||||
outputId = output.pk;
|
||||
} else {
|
||||
outputId = 'untracked';
|
||||
}
|
||||
|
||||
var panel = `#allocation-panel-${outputId}`;
|
||||
|
||||
@ -50,35 +55,42 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
// Add a button to "auto allocate" against the build
|
||||
html += makeIconButton(
|
||||
'fa-magic icon-blue', 'button-output-auto', outputId,
|
||||
'{% trans "Auto-allocate stock items to this output" %}',
|
||||
);
|
||||
// "Auto" allocation only works for untracked stock items
|
||||
if (!output && lines > 0) {
|
||||
html += makeIconButton(
|
||||
'fa-magic icon-blue', 'button-output-auto', outputId,
|
||||
'{% trans "Auto-allocate stock items to this output" %}',
|
||||
);
|
||||
}
|
||||
|
||||
// Add a button to "complete" the particular build output
|
||||
html += makeIconButton(
|
||||
'fa-check icon-green', 'button-output-complete', outputId,
|
||||
'{% trans "Complete build output" %}',
|
||||
{
|
||||
//disabled: true
|
||||
}
|
||||
);
|
||||
if (lines > 0) {
|
||||
// Add a button to "cancel" the particular build output (unallocate)
|
||||
html += makeIconButton(
|
||||
'fa-minus-circle icon-red', 'button-output-unallocate', outputId,
|
||||
'{% trans "Unallocate stock from build output" %}',
|
||||
);
|
||||
}
|
||||
|
||||
// Add a button to "cancel" the particular build output (unallocate)
|
||||
html += makeIconButton(
|
||||
'fa-minus-circle icon-red', 'button-output-unallocate', outputId,
|
||||
'{% trans "Unallocate stock from build output" %}',
|
||||
);
|
||||
|
||||
// Add a button to "delete" the particular build output
|
||||
html += makeIconButton(
|
||||
'fa-trash-alt icon-red', 'button-output-delete', outputId,
|
||||
'{% trans "Delete build output" %}',
|
||||
);
|
||||
if (output) {
|
||||
|
||||
// Add a button to "destroy" the particular build output (mark as damaged, scrap)
|
||||
// TODO
|
||||
// Add a button to "complete" the particular build output
|
||||
html += makeIconButton(
|
||||
'fa-check icon-green', 'button-output-complete', outputId,
|
||||
'{% trans "Complete build output" %}',
|
||||
{
|
||||
//disabled: true
|
||||
}
|
||||
);
|
||||
|
||||
// Add a button to "delete" the particular build output
|
||||
html += makeIconButton(
|
||||
'fa-trash-alt icon-red', 'button-output-delete', outputId,
|
||||
'{% trans "Delete build output" %}',
|
||||
);
|
||||
|
||||
// TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap)
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
@ -90,7 +102,6 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
launchModalForm(`/build/${buildId}/auto-allocate/`,
|
||||
{
|
||||
data: {
|
||||
output: outputId,
|
||||
},
|
||||
success: reloadTable,
|
||||
}
|
||||
@ -98,11 +109,14 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
});
|
||||
|
||||
$(panel).find(`#button-output-complete-${outputId}`).click(function() {
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/build/${buildId}/complete-output/`,
|
||||
{
|
||||
data: {
|
||||
output: outputId,
|
||||
output: pk,
|
||||
},
|
||||
reload: true,
|
||||
}
|
||||
@ -110,24 +124,30 @@ function makeBuildOutputActionButtons(output, buildInfo) {
|
||||
});
|
||||
|
||||
$(panel).find(`#button-output-unallocate-${outputId}`).click(function() {
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/build/${buildId}/unallocate/`,
|
||||
{
|
||||
success: reloadTable,
|
||||
data: {
|
||||
output: outputId,
|
||||
output: pk,
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(panel).find(`#button-output-delete-${outputId}`).click(function() {
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/build/${buildId}/delete-output/`,
|
||||
{
|
||||
reload: true,
|
||||
data: {
|
||||
output: outputId
|
||||
output: pk
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -152,13 +172,21 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
var outputId = null;
|
||||
|
||||
outputId = output.pk;
|
||||
if (output) {
|
||||
outputId = output.pk;
|
||||
} else {
|
||||
outputId = 'untracked';
|
||||
}
|
||||
|
||||
var table = options.table;
|
||||
|
||||
if (options.table == null) {
|
||||
table = `#allocation-table-${outputId}`;
|
||||
}
|
||||
|
||||
// If an "output" is specified, then only "trackable" parts are allocated
|
||||
// Otherwise, only "untrackable" parts are allowed
|
||||
var trackable = ! !output;
|
||||
|
||||
function reloadTable() {
|
||||
// Reload the entire build allocation table
|
||||
@ -168,7 +196,13 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
function requiredQuantity(row) {
|
||||
// Return the requied quantity for a given row
|
||||
|
||||
return row.quantity * output.quantity;
|
||||
if (output) {
|
||||
// "Tracked" parts are calculated against individual build outputs
|
||||
return row.quantity * output.quantity;
|
||||
} else {
|
||||
// "Untracked" parts are specified against the build itself
|
||||
return row.quantity * buildInfo.quantity;
|
||||
}
|
||||
}
|
||||
|
||||
function sumAllocations(row) {
|
||||
@ -300,6 +334,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
queryParams: {
|
||||
part: partId,
|
||||
sub_part_detail: true,
|
||||
sub_part_trackable: trackable,
|
||||
},
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No BOM items found" %}';
|
||||
@ -310,11 +345,19 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
onLoadSuccess: function(tableData) {
|
||||
// Once the BOM data are loaded, request allocation data for this build output
|
||||
|
||||
var params = {
|
||||
build: buildId,
|
||||
}
|
||||
|
||||
if (output) {
|
||||
params.sub_part_trackable = true;
|
||||
params.output = outputId;
|
||||
} else {
|
||||
params.sub_part_trackable = false;
|
||||
}
|
||||
|
||||
inventreeGet('/api/build/item/',
|
||||
{
|
||||
build: buildId,
|
||||
output: outputId,
|
||||
},
|
||||
params,
|
||||
{
|
||||
success: function(data) {
|
||||
// Iterate through the returned data, and group by the part they point to
|
||||
@ -355,8 +398,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
// Calculate the total allocated quantity
|
||||
var allocatedQuantity = sumAllocations(tableRow);
|
||||
|
||||
var requiredQuantity = 0;
|
||||
|
||||
if (output) {
|
||||
requiredQuantity = tableRow.quantity * output.quantity;
|
||||
} else {
|
||||
requiredQuantity = tableRow.quantity * buildInfo.quantity;
|
||||
}
|
||||
|
||||
// Is this line item fully allocated?
|
||||
if (allocatedQuantity >= (tableRow.quantity * output.quantity)) {
|
||||
if (allocatedQuantity >= requiredQuantity) {
|
||||
allocatedLines += 1;
|
||||
}
|
||||
|
||||
@ -367,16 +418,21 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
// Update the total progress for this build output
|
||||
var buildProgress = $(`#allocation-panel-${outputId}`).find($(`#output-progress-${outputId}`));
|
||||
|
||||
var progress = makeProgressBar(
|
||||
allocatedLines,
|
||||
totalLines
|
||||
);
|
||||
if (totalLines > 0) {
|
||||
|
||||
buildProgress.html(progress);
|
||||
var progress = makeProgressBar(
|
||||
allocatedLines,
|
||||
totalLines
|
||||
);
|
||||
|
||||
buildProgress.html(progress);
|
||||
} else {
|
||||
buildProgress.html('');
|
||||
}
|
||||
|
||||
// Update the available actions for this build output
|
||||
|
||||
makeBuildOutputActionButtons(output, buildInfo);
|
||||
makeBuildOutputActionButtons(output, buildInfo, totalLines);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -600,6 +656,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
// Initialize the action buttons
|
||||
makeBuildOutputActionButtons(output, buildInfo, 0);
|
||||
}
|
||||
|
||||
|
||||
@ -654,7 +713,7 @@ function loadBuildTable(table, options) {
|
||||
field: 'reference',
|
||||
title: '{% trans "Build" %}',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
switchable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
|
||||
@ -675,6 +734,7 @@ function loadBuildTable(table, options) {
|
||||
{
|
||||
field: 'title',
|
||||
title: '{% trans "Description" %}',
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
field: 'part',
|
||||
@ -725,7 +785,7 @@ function loadBuildTable(table, options) {
|
||||
},
|
||||
{
|
||||
field: 'completion_date',
|
||||
title: '{% trans "Completed" %}',
|
||||
title: '{% trans "Completion Date" %}',
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
/* Part API functions
|
||||
* Requires api.js to be loaded first
|
||||
@ -506,6 +507,82 @@ function loadPartTable(table, url, options={}) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadPartCategoryTable(table, options) {
|
||||
/* Display a table of part categories */
|
||||
|
||||
var params = options.params || {};
|
||||
|
||||
var filterListElement = options.filterList || '#filter-list-category';
|
||||
|
||||
var filters = {};
|
||||
|
||||
var filterKey = options.filterKey || options.name || 'category';
|
||||
|
||||
if (!options.disableFilters) {
|
||||
filters = loadTableFilters(filterKey);
|
||||
}
|
||||
|
||||
var original = {};
|
||||
|
||||
for (var key in params) {
|
||||
original[key] = params[key];
|
||||
filters[key] = params[key];
|
||||
}
|
||||
|
||||
setupFilterList(filterKey, table, filterListElement);
|
||||
|
||||
table.inventreeTable({
|
||||
method: 'get',
|
||||
url: options.url || '{% url "api-part-category-list" %}',
|
||||
queryParams: filters,
|
||||
sidePagination: 'server',
|
||||
name: 'category',
|
||||
original: original,
|
||||
showColumns: true,
|
||||
columns: [
|
||||
{
|
||||
checkbox: true,
|
||||
title: '{% trans "Select" %}',
|
||||
searchable: false,
|
||||
switchable: false,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '{% trans "Name" %}',
|
||||
switchable: true,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
return renderLink(
|
||||
value,
|
||||
`/part/category/${row.pk}/`
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '{% trans "Description" %}',
|
||||
switchable: true,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'pathstring',
|
||||
title: '{% trans "Path" %}',
|
||||
switchable: true,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'parts',
|
||||
title: '{% trans "Parts" %}',
|
||||
switchable: true,
|
||||
sortable: false,
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function yesNoLabel(value) {
|
||||
if (value) {
|
||||
return `<span class='label label-green'>{% trans "YES" %}</span>`;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user