mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-19 13:20:37 -06:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57eada1da1 | ||
|
|
f526dcdeec | ||
|
|
aacf35ed47 | ||
|
|
ca986cba01 | ||
|
|
699fb83dd4 | ||
|
|
dd6e225cda | ||
|
|
1f3a49b1ae | ||
|
|
385e7cb478 | ||
|
|
73768bfee1 | ||
|
|
946fe2df29 | ||
|
|
afa7ed873f | ||
|
|
46da332afe | ||
|
|
072b7b3146 |
@@ -31,3 +31,15 @@ class InvenTreeResource(ModelResource):
|
||||
row[idx] = val
|
||||
|
||||
return row
|
||||
|
||||
def get_fields(self, **kwargs):
|
||||
"""Return fields, with some common exclusions"""
|
||||
|
||||
fields = super().get_fields(**kwargs)
|
||||
|
||||
fields_to_exclude = [
|
||||
'metadata',
|
||||
'lft', 'rght', 'tree_id', 'level',
|
||||
]
|
||||
|
||||
return [f for f in fields if f.column_name not in fields_to_exclude]
|
||||
|
||||
@@ -59,14 +59,39 @@ class NotFoundView(AjaxView):
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Process an `not found` event on the API."""
|
||||
data = {
|
||||
'details': _('API endpoint not found'),
|
||||
'url': request.build_absolute_uri(),
|
||||
}
|
||||
def not_found(self, request):
|
||||
"""Return a 404 error"""
|
||||
return JsonResponse(
|
||||
{
|
||||
'detail': _('API endpoint not found'),
|
||||
'url': request.build_absolute_uri(),
|
||||
},
|
||||
status=404
|
||||
)
|
||||
|
||||
return JsonResponse(data, status=404)
|
||||
def options(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
return self.not_found(request)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
return self.not_found(request)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
return self.not_found(request)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
return self.not_found(request)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
return self.not_found(request)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Return 404"""
|
||||
return self.not_found(request)
|
||||
|
||||
|
||||
class BulkDeleteMixin:
|
||||
|
||||
@@ -292,6 +292,15 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, Default
|
||||
|
||||
return False
|
||||
|
||||
def get_email_confirmation_url(self, request, emailconfirmation):
|
||||
"""Construct the email confirmation url"""
|
||||
|
||||
from InvenTree.helpers_model import construct_absolute_url
|
||||
|
||||
url = super().get_email_confirmation_url(request, emailconfirmation)
|
||||
url = construct_absolute_url(url)
|
||||
return url
|
||||
|
||||
|
||||
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
|
||||
"""Override of adapter to use dynamic settings."""
|
||||
|
||||
@@ -50,9 +50,7 @@ def construct_absolute_url(*arg, **kwargs):
|
||||
# Otherwise, try to use the InvenTree setting
|
||||
try:
|
||||
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
||||
except ProgrammingError:
|
||||
pass
|
||||
except OperationalError:
|
||||
except (ProgrammingError, OperationalError):
|
||||
pass
|
||||
|
||||
if not site_url:
|
||||
|
||||
@@ -601,6 +601,8 @@ DATABASES = {
|
||||
REMOTE_LOGIN = get_boolean_setting('INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False)
|
||||
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER')
|
||||
|
||||
LOGIN_REDIRECT_URL = "/index/"
|
||||
|
||||
# sentry.io integration for error reporting
|
||||
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ from django.contrib import admin
|
||||
from django.urls import include, path, re_path
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from dj_rest_auth.registration.views import (SocialAccountDisconnectView,
|
||||
from dj_rest_auth.registration.views import (ConfirmEmailView,
|
||||
SocialAccountDisconnectView,
|
||||
SocialAccountListView)
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
|
||||
|
||||
@@ -74,13 +75,16 @@ apipatterns = [
|
||||
# InvenTree information endpoint
|
||||
path('', InfoView.as_view(), name='api-inventree-info'),
|
||||
|
||||
# Third party API endpoints
|
||||
path('auth/', include('dj_rest_auth.urls')),
|
||||
path('auth/registration/', include('dj_rest_auth.registration.urls')),
|
||||
path('auth/providers/', SocialProvierListView.as_view(), name='social_providers'),
|
||||
path('auth/social/', include(social_auth_urlpatterns)),
|
||||
path('auth/social/', SocialAccountListView.as_view(), name='social_account_list'),
|
||||
path('auth/social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
|
||||
# Auth API endpoints
|
||||
path('auth/', include([
|
||||
re_path(r'^registration/account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'),
|
||||
path('registration/', include('dj_rest_auth.registration.urls')),
|
||||
path('providers/', SocialProvierListView.as_view(), name='social_providers'),
|
||||
path('social/', include(social_auth_urlpatterns)),
|
||||
path('social/', SocialAccountListView.as_view(), name='social_account_list'),
|
||||
path('social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
|
||||
path('', include('dj_rest_auth.urls')),
|
||||
])),
|
||||
|
||||
# Unknown endpoint
|
||||
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
||||
|
||||
@@ -18,7 +18,7 @@ from dulwich.repo import NotGitRepository, Repo
|
||||
from .api_version import INVENTREE_API_VERSION
|
||||
|
||||
# InvenTree software version
|
||||
INVENTREE_SW_VERSION = "0.12.2"
|
||||
INVENTREE_SW_VERSION = "0.12.4"
|
||||
|
||||
# Discover git
|
||||
try:
|
||||
|
||||
@@ -719,14 +719,22 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
if items.exists() and items.count() == 1:
|
||||
stock_item = items[0]
|
||||
|
||||
# Allocate the stock item
|
||||
BuildItem.objects.create(
|
||||
build=self,
|
||||
bom_item=bom_item,
|
||||
stock_item=stock_item,
|
||||
quantity=1,
|
||||
install_into=output,
|
||||
)
|
||||
# Find the 'BuildLine' object which points to this BomItem
|
||||
try:
|
||||
build_line = BuildLine.objects.get(
|
||||
build=self,
|
||||
bom_item=bom_item
|
||||
)
|
||||
|
||||
# Allocate the stock items against the BuildLine
|
||||
BuildItem.objects.create(
|
||||
build_line=build_line,
|
||||
stock_item=stock_item,
|
||||
quantity=1,
|
||||
install_into=output,
|
||||
)
|
||||
except BuildLine.DoesNotExist:
|
||||
pass
|
||||
|
||||
else:
|
||||
"""Create a single build output of the given quantity."""
|
||||
|
||||
@@ -171,6 +171,14 @@ class PurchaseOrderLineItemResource(PriceResourceMixin, InvenTreeResource):
|
||||
|
||||
SKU = Field(attribute='part__SKU', readonly=True)
|
||||
|
||||
def dehydrate_purchase_price(self, line):
|
||||
"""Return a string value of the 'purchase_price' field, rather than the 'Money' object"""
|
||||
|
||||
if line.purchase_price:
|
||||
return line.purchase_price.amount
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
class PurchaseOrderExtraLineResource(PriceResourceMixin, InvenTreeResource):
|
||||
"""Class for managing import / export of PurchaseOrderExtraLine data."""
|
||||
|
||||
@@ -2031,10 +2031,6 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
if bom_item.part in my_ancestors and bom_item.inherited:
|
||||
continue
|
||||
|
||||
# Skip if already exists
|
||||
if BomItem.objects.filter(part=self, sub_part=bom_item.sub_part).exists():
|
||||
continue
|
||||
|
||||
# Skip (or throw error) if BomItem is not valid
|
||||
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
|
||||
continue
|
||||
|
||||
@@ -38,9 +38,13 @@ def sso_check_provider(provider):
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
|
||||
# First, check that the provider is enabled
|
||||
apps = SocialApp.objects.filter(provider__iexact=provider.name)
|
||||
apps = SocialApp.objects.filter(provider__iexact=provider.id)
|
||||
|
||||
if not apps.exists():
|
||||
logging.error(
|
||||
"SSO SocialApp %s does not exist (known providers: %s)",
|
||||
provider.id, [obj.provider for obj in SocialApp.objects.all()]
|
||||
)
|
||||
return False
|
||||
|
||||
# Next, check that the provider is correctly configured
|
||||
|
||||
@@ -18,6 +18,7 @@ import InvenTree.helpers
|
||||
import order.models
|
||||
import part.models
|
||||
from InvenTree.api import MetadataView
|
||||
from InvenTree.exceptions import log_error
|
||||
from InvenTree.filters import InvenTreeSearchFilter
|
||||
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
|
||||
from stock.models import StockItem, StockItemAttachment
|
||||
@@ -181,78 +182,90 @@ class ReportPrintMixin:
|
||||
# Start with a default report name
|
||||
report_name = "report.pdf"
|
||||
|
||||
# Merge one or more PDF files into a single download
|
||||
for item in items_to_print:
|
||||
report = self.get_object()
|
||||
report.object_to_print = item
|
||||
try:
|
||||
# Merge one or more PDF files into a single download
|
||||
for item in items_to_print:
|
||||
report = self.get_object()
|
||||
report.object_to_print = item
|
||||
|
||||
report_name = report.generate_filename(request)
|
||||
output = report.render(request)
|
||||
report_name = report.generate_filename(request)
|
||||
output = report.render(request)
|
||||
|
||||
# Run report callback for each generated report
|
||||
self.report_callback(item, output, request)
|
||||
# Run report callback for each generated report
|
||||
self.report_callback(item, output, request)
|
||||
|
||||
try:
|
||||
if debug_mode:
|
||||
outputs.append(report.render_as_string(request))
|
||||
else:
|
||||
outputs.append(output)
|
||||
except TemplateDoesNotExist as e:
|
||||
template = str(e)
|
||||
if not template:
|
||||
template = report.template
|
||||
try:
|
||||
if debug_mode:
|
||||
outputs.append(report.render_as_string(request))
|
||||
else:
|
||||
outputs.append(output)
|
||||
except TemplateDoesNotExist as e:
|
||||
template = str(e)
|
||||
if not template:
|
||||
template = report.template
|
||||
|
||||
return Response(
|
||||
{
|
||||
'error': _(f"Template file '{template}' is missing or does not exist"),
|
||||
},
|
||||
status=400,
|
||||
return Response(
|
||||
{
|
||||
'error': _(f"Template file '{template}' is missing or does not exist"),
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if not report_name.endswith('.pdf'):
|
||||
report_name += '.pdf'
|
||||
|
||||
if debug_mode:
|
||||
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
|
||||
|
||||
html = "\n".join(outputs)
|
||||
|
||||
return HttpResponse(html)
|
||||
else:
|
||||
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
|
||||
|
||||
pages = []
|
||||
|
||||
try:
|
||||
for output in outputs:
|
||||
doc = output.get_document()
|
||||
for page in doc.pages:
|
||||
pages.append(page)
|
||||
|
||||
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
||||
|
||||
except TemplateDoesNotExist as e:
|
||||
|
||||
template = str(e)
|
||||
|
||||
if not template:
|
||||
template = report.template
|
||||
|
||||
return Response(
|
||||
{
|
||||
'error': _(f"Template file '{template}' is missing or does not exist"),
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user, cache=False)
|
||||
|
||||
return InvenTree.helpers.DownloadFile(
|
||||
pdf,
|
||||
report_name,
|
||||
content_type='application/pdf',
|
||||
inline=inline,
|
||||
)
|
||||
|
||||
if not report_name.endswith('.pdf'):
|
||||
report_name += '.pdf'
|
||||
except Exception as exc:
|
||||
# Log the exception to the database
|
||||
log_error(request.path)
|
||||
|
||||
if debug_mode:
|
||||
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
|
||||
|
||||
html = "\n".join(outputs)
|
||||
|
||||
return HttpResponse(html)
|
||||
else:
|
||||
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
|
||||
|
||||
pages = []
|
||||
|
||||
try:
|
||||
for output in outputs:
|
||||
doc = output.get_document()
|
||||
for page in doc.pages:
|
||||
pages.append(page)
|
||||
|
||||
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
||||
|
||||
except TemplateDoesNotExist as e:
|
||||
|
||||
template = str(e)
|
||||
|
||||
if not template:
|
||||
template = report.template
|
||||
|
||||
return Response(
|
||||
{
|
||||
'error': _(f"Template file '{template}' is missing or does not exist"),
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user, cache=False)
|
||||
|
||||
return InvenTree.helpers.DownloadFile(
|
||||
pdf,
|
||||
report_name,
|
||||
content_type='application/pdf',
|
||||
inline=inline,
|
||||
)
|
||||
# Re-throw the exception to the client as a DRF exception
|
||||
raise ValidationError({
|
||||
'error': 'Report printing failed',
|
||||
'detail': str(exc),
|
||||
'path': request.path,
|
||||
})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Default implementation of GET for a print endpoint.
|
||||
|
||||
@@ -69,6 +69,7 @@ def fix_purchase_price(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0047_supplierpart_pack_size'),
|
||||
('stock', '0093_auto_20230217_2140'),
|
||||
]
|
||||
|
||||
|
||||
@@ -905,6 +905,18 @@ function loadBomTable(table, options={}) {
|
||||
title: '{% trans "Part" %}',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
sorter: function(_valA, _valB, rowA, rowB) {
|
||||
let name_a = rowA.sub_part_detail.full_name;
|
||||
let name_b = rowB.sub_part_detail.full_name;
|
||||
|
||||
if (name_a > name_b) {
|
||||
return 1;
|
||||
} else if (name_a < name_b) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
formatter: function(value, row) {
|
||||
var url = `/part/${row.sub_part}/`;
|
||||
var html = '';
|
||||
|
||||
2
Procfile
2
Procfile
@@ -1,3 +1,3 @@
|
||||
web: env/bin/gunicorn --chdir $APP_HOME/InvenTree -c InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
|
||||
worker: env/bin/python InvenTree/manage.py qcluster
|
||||
cli: . env/bin/activate && exec env/bin/python -m invoke
|
||||
cli: echo "" && . env/bin/activate && exec env/bin/python -m invoke
|
||||
|
||||
@@ -17,7 +17,7 @@ In addition to providing the ability for end-users to provide their own reportin
|
||||
InvenTree report templates utilize the powerful [WeasyPrint](https://weasyprint.org/) PDF generation engine.
|
||||
|
||||
!!! info "WeasyPrint"
|
||||
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://weasyprint.readthedocs.io/en/stable/) for further information.
|
||||
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/) for further information.
|
||||
|
||||
### Stylesheets
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ sudo apt-get install \
|
||||
```
|
||||
|
||||
!!! warning "Weasyprint"
|
||||
On some systems, the dependencies for the `weasyprint` package might not be installed. Consider running through the [weasyprint installation steps](https://weasyprint.readthedocs.io/en/stable/install.html) before moving forward.
|
||||
On some systems, the dependencies for the `weasyprint` package might not be installed. Consider running through the [weasyprint installation steps](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation) before moving forward.
|
||||
|
||||
|
||||
### Create InvenTree User
|
||||
|
||||
Reference in New Issue
Block a user