Compare commits

...

13 Commits

Author SHA1 Message Date
Oliver
57eada1da1 backport email fix (#5409)
- Backport of https://github.com/inventree/InvenTree/pull/5396
2023-08-08 15:19:00 +10:00
Oliver
f526dcdeec fix cli on 22.04 (#5204) (#5395)
* fix cli on 22.04 (#5204)

(cherry picked from commit d4fad4f5c8)

* Update weasyprint docs link

* Another link fix

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2023-08-03 16:21:53 +10:00
github-actions[bot]
aacf35ed47 Improve sorting of part column for BOM table (#5386) (#5387)
(cherry picked from commit c39ae80a13)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-02 17:43:08 +10:00
github-actions[bot]
ca986cba01 Fix auto-allocation of build outputs (#5378) (#5379)
- Creation of BuildItem objects was using old model references

(cherry picked from commit 668dab4175)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-08-01 11:26:10 +10:00
github-actions[bot]
699fb83dd4 Fix SSO check comparing id against name and extend log output (#5340) (#5377)
* add error log on SSO check failure

* sso_check_provider: fix by comparing against id

the name is the pretty printed version which not necessarily is the same
as the provider id it is compared against. This fails e.g. for the
microsoft allauth extension where the id is microsoft, but the name is
"Microsoft Graph".

Closes: #5330
(cherry picked from commit ee5416719f)

Co-authored-by: Hendrik v. Raven <hendrik@consetetur.de>
2023-08-01 10:39:46 +10:00
Oliver
dd6e225cda Update version.py (#5374)
Bump version number to 0.12.4
2023-07-31 12:45:49 +10:00
github-actions[bot]
1f3a49b1ae Fix for migration - updating from old version (#5372) (#5373)
(cherry picked from commit 90383ccb53)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-31 12:43:20 +10:00
github-actions[bot]
385e7cb478 Return 404 on API requests other than GET (#5365) (#5366)
- Other request methods need love too!

(cherry picked from commit 59ffdcaa19)
(cherry picked from commit b89a120f9e)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 22:14:55 +10:00
github-actions[bot]
73768bfee1 Handle purchase price export for .xls files (#5362) (#5363)
(cherry picked from commit 87da286f2f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 15:36:34 +10:00
github-actions[bot]
946fe2df29 Handle errors when printing reports (#5360) (#5361)
- Re-throw as a ValidationError
- Results in a 400 error, not a 500

(cherry picked from commit 5f3d3b28b3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-28 14:23:43 +10:00
github-actions[bot]
afa7ed873f Exclude some common fields from django-import-export (#5349) (#5351)
- Add "get_fields()" method to InvenTreeResource
- Override default behaviour and exclude some common fields
- Will flow down to any inheriting classes

(cherry picked from commit 941451203a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-26 17:22:21 +10:00
github-actions[bot]
46da332afe Allow duplicate BOM items when duplicating a part (#5347) (#5350)
(cherry picked from commit 6660508326)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2023-07-26 16:54:20 +10:00
Oliver
072b7b3146 Update version.py
Bump version number to 0.12.3
2023-07-25 11:46:41 +10:00
17 changed files with 192 additions and 100 deletions

View File

@@ -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]

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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)

View File

@@ -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'),

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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'),
]

View File

@@ -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 = '';

View File

@@ -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

View File

@@ -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

View File

@@ -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