mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-14 19:15:41 +00:00
Typed report context (#9431)
* add typed report context * make it py3.9 compatible * fix docs * debug docs * fix for py 3.9 * add requested error codes
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
|
||||
import io
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from typing import Optional, cast
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
@ -27,7 +27,7 @@ from InvenTree.format import format_money
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
def get_base_url(request=None):
|
||||
def get_base_url(request=None) -> str:
|
||||
"""Return the base URL for the InvenTree server.
|
||||
|
||||
The base URL is determined in the following order of decreasing priority:
|
||||
@ -56,7 +56,7 @@ def get_base_url(request=None):
|
||||
# Check if a global InvenTree setting is provided
|
||||
try:
|
||||
if site_url := get_global_setting('INVENTREE_BASE_URL', create=False):
|
||||
return site_url
|
||||
return cast(str, site_url)
|
||||
except (ProgrammingError, OperationalError):
|
||||
pass
|
||||
|
||||
|
@ -0,0 +1,141 @@
|
||||
"""Custom management command to export the available report context.
|
||||
|
||||
This is used to generate a JSON file which contains all available report
|
||||
context, so that they can be introspected by the InvenTree documentation system.
|
||||
|
||||
This in turn allows report context to be documented in the InvenTree documentation,
|
||||
without having to manually duplicate the information in multiple places.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import get_args, get_origin, get_type_hints
|
||||
|
||||
import django.db.models
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from report.mixins import QuerySet
|
||||
|
||||
|
||||
def get_type_str(type_obj):
|
||||
"""Get the type str of a type object, including any generic parameters."""
|
||||
if type_obj is django.db.models.QuerySet:
|
||||
raise CommandError(
|
||||
'INVE-E3 - Do not use django.db.models.QuerySet directly for typing, use report.mixins.QuerySet instead.'
|
||||
)
|
||||
|
||||
if origin := get_origin(type_obj):
|
||||
# use abbreviated name for QuerySet to save space
|
||||
origin_str = 'QuerySet' if origin is QuerySet else get_type_str(origin)
|
||||
|
||||
return f'{origin_str}[{", ".join(get_type_str(arg) for arg in get_args(type_obj))}]'
|
||||
|
||||
if type_obj is type(None):
|
||||
return 'None'
|
||||
|
||||
if type_obj.__module__ == 'builtins':
|
||||
return type_obj.__name__
|
||||
|
||||
# in python3.9, typing.Union has no __name__
|
||||
if not hasattr(type_obj, '__module__') or not hasattr(type_obj, '__name__'):
|
||||
return str(type_obj)
|
||||
|
||||
return f'{type_obj.__module__}.{type_obj.__name__}'
|
||||
|
||||
|
||||
def parse_docstring(docstring: str):
|
||||
"""Parse the docstring of a type object and return a dictionary of sections."""
|
||||
sections = {}
|
||||
current_section = None
|
||||
|
||||
for line in docstring.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
if stripped.endswith(':'):
|
||||
current_section = stripped.rstrip(':')
|
||||
sections[current_section] = {}
|
||||
elif ':' in stripped and current_section:
|
||||
name, doc = stripped.split(':', 1)
|
||||
sections[current_section][name.strip()] = doc.strip()
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Extract report context information, and export to a JSON file."""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add custom arguments for this command."""
|
||||
parser.add_argument(
|
||||
'filename',
|
||||
type=str,
|
||||
help='Output filename for the report context definitions',
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Export report context information to a JSON file."""
|
||||
from report.helpers import report_model_types
|
||||
from report.models import (
|
||||
BaseContextExtension,
|
||||
LabelContextExtension,
|
||||
ReportContextExtension,
|
||||
)
|
||||
|
||||
context = {'models': {}, 'base': {}}
|
||||
is_error = False
|
||||
|
||||
# Base context models
|
||||
for key, model in [
|
||||
('global', BaseContextExtension),
|
||||
('report', ReportContextExtension),
|
||||
('label', LabelContextExtension),
|
||||
]:
|
||||
context['base'][key] = {'key': key, 'context': {}}
|
||||
|
||||
attributes = parse_docstring(model.__doc__).get('Attributes', {})
|
||||
for k, v in get_type_hints(model).items():
|
||||
context['base'][key]['context'][k] = {
|
||||
'description': attributes.get(k, ''),
|
||||
'type': get_type_str(v),
|
||||
}
|
||||
|
||||
# Report context models
|
||||
for model in report_model_types():
|
||||
model_key = model.__name__.lower()
|
||||
model_name = str(model._meta.verbose_name)
|
||||
|
||||
if (
|
||||
ctx_type := get_type_hints(model.report_context).get('return', None)
|
||||
) is None:
|
||||
print(
|
||||
f'Error: Model {model}.report_context does not have a return type annotation'
|
||||
)
|
||||
is_error = True
|
||||
continue
|
||||
|
||||
context['models'][model_key] = {
|
||||
'key': model_key,
|
||||
'name': model_name,
|
||||
'context': {},
|
||||
}
|
||||
|
||||
attributes = parse_docstring(ctx_type.__doc__).get('Attributes', {})
|
||||
for k, v in get_type_hints(ctx_type).items():
|
||||
context['models'][model_key]['context'][k] = {
|
||||
'description': attributes.get(k, ''),
|
||||
'type': get_type_str(v),
|
||||
}
|
||||
|
||||
if is_error:
|
||||
raise CommandError(
|
||||
'INVE-E4 - Some models associated with the `InvenTreeReportMixin` do not have a valid `report_context` return type annotation.'
|
||||
)
|
||||
|
||||
filename = kwargs.get('filename', 'inventree_report_context.json')
|
||||
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(context, f, indent=4)
|
||||
|
||||
print(f"Exported InvenTree report context definitions to '{filename}'")
|
@ -947,7 +947,7 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
)
|
||||
|
||||
def format_barcode(self, **kwargs):
|
||||
"""Return a JSON string for formatting a QR code for this model instance."""
|
||||
"""Return a string for formatting a QR code for this model instance."""
|
||||
from plugin.base.barcodes.helper import generate_barcode
|
||||
|
||||
return generate_barcode(self)
|
||||
@ -966,7 +966,7 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
return data
|
||||
|
||||
@property
|
||||
def barcode(self):
|
||||
def barcode(self) -> str:
|
||||
"""Format a minimal barcode string (e.g. for label printing)."""
|
||||
return self.format_barcode()
|
||||
|
||||
|
@ -49,6 +49,30 @@ from stock.status_codes import StockHistoryCode, StockStatus
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
class BuildReportContext(report.mixins.BaseReportContext):
|
||||
"""Context for the Build model.
|
||||
|
||||
Attributes:
|
||||
bom_items: Query set of all BuildItem objects associated with the BuildOrder
|
||||
build: The BuildOrder instance itself
|
||||
build_outputs: Query set of all BuildItem objects associated with the BuildOrder
|
||||
line_items: Query set of all build line items associated with the BuildOrder
|
||||
part: The Part object which is being assembled in the build order
|
||||
quantity: The total quantity of the part being assembled
|
||||
reference: The reference field of the BuildOrder
|
||||
title: The title field of the BuildOrder
|
||||
"""
|
||||
|
||||
bom_items: report.mixins.QuerySet[part.models.BomItem]
|
||||
build: 'Build'
|
||||
build_outputs: report.mixins.QuerySet[stock.models.StockItem]
|
||||
line_items: report.mixins.QuerySet['BuildLine']
|
||||
part: part.models.Part
|
||||
quantity: int
|
||||
reference: str
|
||||
title: str
|
||||
|
||||
|
||||
class Build(
|
||||
report.mixins.InvenTreeReportMixin,
|
||||
InvenTree.models.InvenTreeAttachmentMixin,
|
||||
@ -183,7 +207,7 @@ class Build(
|
||||
'target_date': _('Target date must be after start date')
|
||||
})
|
||||
|
||||
def report_context(self) -> dict:
|
||||
def report_context(self) -> BuildReportContext:
|
||||
"""Generate custom report context data."""
|
||||
return {
|
||||
'bom_items': self.part.get_bom_items(),
|
||||
@ -1454,6 +1478,28 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
||||
instance.update_build_line_items()
|
||||
|
||||
|
||||
class BuildLineReportContext(report.mixins.BaseReportContext):
|
||||
"""Context for the BuildLine model.
|
||||
|
||||
Attributes:
|
||||
allocated_quantity: The quantity of the part which has been allocated to this build
|
||||
allocations: A query set of all StockItem objects which have been allocated to this build line
|
||||
bom_item: The BomItem associated with this line item
|
||||
build: The BuildOrder instance associated with this line item
|
||||
build_line: The build line instance itself
|
||||
part: The sub-part (component) associated with the linked BomItem instance
|
||||
quantity: The quantity required for this line item
|
||||
"""
|
||||
|
||||
allocated_quantity: decimal.Decimal
|
||||
allocations: report.mixins.QuerySet['BuildItem']
|
||||
bom_item: part.models.BomItem
|
||||
build: Build
|
||||
build_line: 'BuildLine'
|
||||
part: part.models.Part
|
||||
quantity: decimal.Decimal
|
||||
|
||||
|
||||
class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeModel):
|
||||
"""A BuildLine object links a BOMItem to a Build.
|
||||
|
||||
@ -1481,7 +1527,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
|
||||
"""Return the API URL used to access this model."""
|
||||
return reverse('api-build-line-list')
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> BuildLineReportContext:
|
||||
"""Generate custom report context for this BuildLine object."""
|
||||
return {
|
||||
'allocated_quantity': self.allocated_quantity,
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Order model definitions."""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -177,6 +178,92 @@ class TotalPriceMixin(models.Model):
|
||||
return total
|
||||
|
||||
|
||||
class BaseOrderReportContext(report.mixins.BaseReportContext):
|
||||
"""Base context for all order models.
|
||||
|
||||
Attributes:
|
||||
description: The description field of the order
|
||||
extra_lines: Query set of all extra lines associated with the order
|
||||
lines: Query set of all line items associated with the order
|
||||
order: The order instance itself
|
||||
reference: The reference field of the order
|
||||
title: The title (string representation) of the order
|
||||
"""
|
||||
|
||||
description: str
|
||||
extra_lines: Any
|
||||
lines: Any
|
||||
order: Any
|
||||
reference: str
|
||||
title: str
|
||||
|
||||
|
||||
class PurchaseOrderReportContext(report.mixins.BaseReportContext):
|
||||
"""Context for the purchase order model.
|
||||
|
||||
Attributes:
|
||||
description: The description field of the PurchaseOrder
|
||||
reference: The reference field of the PurchaseOrder
|
||||
title: The title (string representation) of the PurchaseOrder
|
||||
extra_lines: Query set of all extra lines associated with the PurchaseOrder
|
||||
lines: Query set of all line items associated with the PurchaseOrder
|
||||
order: The PurchaseOrder instance itself
|
||||
supplier: The supplier object associated with the PurchaseOrder
|
||||
"""
|
||||
|
||||
description: str
|
||||
reference: str
|
||||
title: str
|
||||
extra_lines: report.mixins.QuerySet['PurchaseOrderExtraLine']
|
||||
lines: report.mixins.QuerySet['PurchaseOrderLineItem']
|
||||
order: 'PurchaseOrder'
|
||||
supplier: Optional[Company]
|
||||
|
||||
|
||||
class SalesOrderReportContext(report.mixins.BaseReportContext):
|
||||
"""Context for the sales order model.
|
||||
|
||||
Attributes:
|
||||
description: The description field of the SalesOrder
|
||||
reference: The reference field of the SalesOrder
|
||||
title: The title (string representation) of the SalesOrder
|
||||
extra_lines: Query set of all extra lines associated with the SalesOrder
|
||||
lines: Query set of all line items associated with the SalesOrder
|
||||
order: The SalesOrder instance itself
|
||||
customer: The customer object associated with the SalesOrder
|
||||
"""
|
||||
|
||||
description: str
|
||||
reference: str
|
||||
title: str
|
||||
extra_lines: report.mixins.QuerySet['SalesOrderExtraLine']
|
||||
lines: report.mixins.QuerySet['SalesOrderLineItem']
|
||||
order: 'SalesOrder'
|
||||
customer: Optional[Company]
|
||||
|
||||
|
||||
class ReturnOrderReportContext(report.mixins.BaseReportContext):
|
||||
"""Context for the return order model.
|
||||
|
||||
Attributes:
|
||||
description: The description field of the ReturnOrder
|
||||
reference: The reference field of the ReturnOrder
|
||||
title: The title (string representation) of the ReturnOrder
|
||||
extra_lines: Query set of all extra lines associated with the ReturnOrder
|
||||
lines: Query set of all line items associated with the ReturnOrder
|
||||
order: The ReturnOrder instance itself
|
||||
customer: The customer object associated with the ReturnOrder
|
||||
"""
|
||||
|
||||
description: str
|
||||
reference: str
|
||||
title: str
|
||||
extra_lines: report.mixins.QuerySet['ReturnOrderExtraLine']
|
||||
lines: report.mixins.QuerySet['ReturnOrderLineItem']
|
||||
order: 'ReturnOrder'
|
||||
customer: Optional[Company]
|
||||
|
||||
|
||||
class Order(
|
||||
StatusCodeMixin,
|
||||
StateTransitionMixin,
|
||||
@ -300,7 +387,7 @@ class Order(
|
||||
line.target_date = None
|
||||
line.order = self
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> BaseOrderReportContext:
|
||||
"""Generate context data for the reporting interface."""
|
||||
return {
|
||||
'description': self.description,
|
||||
@ -456,7 +543,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
super().clean_line_item(line)
|
||||
line.received = 0
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> PurchaseOrderReportContext:
|
||||
"""Return report context data for this PurchaseOrder."""
|
||||
return {**super().report_context(), 'supplier': self.supplier}
|
||||
|
||||
@ -979,7 +1066,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
super().clean_line_item(line)
|
||||
line.shipped = 0
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> SalesOrderReportContext:
|
||||
"""Generate report context data for this SalesOrder."""
|
||||
return {**super().report_context(), 'customer': self.customer}
|
||||
|
||||
@ -1802,6 +1889,26 @@ class SalesOrderLineItem(OrderLineItem):
|
||||
return self.shipped >= self.quantity
|
||||
|
||||
|
||||
class SalesOrderShipmentReportContext(report.mixins.BaseReportContext):
|
||||
"""Context for the SalesOrderShipment model.
|
||||
|
||||
Attributes:
|
||||
allocations: QuerySet of SalesOrderAllocation objects
|
||||
order: The associated SalesOrder object
|
||||
reference: Shipment reference string
|
||||
shipment: The SalesOrderShipment object itself
|
||||
tracking_number: Shipment tracking number string
|
||||
title: Title for the report
|
||||
"""
|
||||
|
||||
allocations: report.mixins.QuerySet['SalesOrderAllocation']
|
||||
order: 'SalesOrder'
|
||||
reference: str
|
||||
shipment: 'SalesOrderShipment'
|
||||
tracking_number: str
|
||||
title: str
|
||||
|
||||
|
||||
class SalesOrderShipment(
|
||||
InvenTree.models.InvenTreeAttachmentMixin,
|
||||
InvenTree.models.InvenTreeNotesMixin,
|
||||
@ -1835,7 +1942,7 @@ class SalesOrderShipment(
|
||||
"""Return the API URL associated with the SalesOrderShipment model."""
|
||||
return reverse('api-so-shipment-list')
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> SalesOrderShipmentReportContext:
|
||||
"""Generate context data for the reporting interface."""
|
||||
return {
|
||||
'allocations': self.allocations,
|
||||
@ -2198,7 +2305,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
line.received_date = None
|
||||
line.outcome = ReturnOrderLineStatus.PENDING.value
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> ReturnOrderReportContext:
|
||||
"""Generate report context data for this ReturnOrder."""
|
||||
return {**super().report_context(), 'customer': self.customer}
|
||||
|
||||
|
@ -10,13 +10,14 @@ import os
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Optional, cast
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinLengthValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.db.models import ExpressionWrapper, F, Q, Sum, UniqueConstraint
|
||||
from django.db.models import ExpressionWrapper, F, Q, QuerySet, Sum, UniqueConstraint
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.db.utils import IntegrityError
|
||||
@ -359,6 +360,38 @@ class PartManager(TreeManager):
|
||||
)
|
||||
|
||||
|
||||
class PartReportContext(report.mixins.BaseReportContext):
|
||||
"""Context for the part model.
|
||||
|
||||
Attributes:
|
||||
bom_items: Query set of all BomItem objects associated with the Part
|
||||
category: The PartCategory object associated with the Part
|
||||
description: The description field of the Part
|
||||
IPN: The IPN (internal part number) of the Part
|
||||
name: The name of the Part
|
||||
parameters: Dict object containing the parameters associated with the Part
|
||||
part: The Part object itself
|
||||
qr_data: Formatted QR code data for the Part
|
||||
qr_url: Generated URL for embedding in a QR code
|
||||
revision: The revision of the Part
|
||||
test_template_list: List of test templates associated with the Part
|
||||
test_templates: Dict object of test templates associated with the Part
|
||||
"""
|
||||
|
||||
bom_items: report.mixins.QuerySet[BomItem]
|
||||
category: Optional[PartCategory]
|
||||
description: str
|
||||
IPN: Optional[str]
|
||||
name: str
|
||||
parameters: dict[str, str]
|
||||
part: Part
|
||||
qr_data: str
|
||||
qr_url: str
|
||||
revision: Optional[str]
|
||||
test_template_list: report.mixins.QuerySet[PartTestTemplate]
|
||||
test_templates: dict[str, PartTestTemplate]
|
||||
|
||||
|
||||
@cleanup.ignore
|
||||
class Part(
|
||||
InvenTree.models.InvenTreeAttachmentMixin,
|
||||
@ -441,10 +474,10 @@ class Part(
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return 'PA'
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> PartReportContext:
|
||||
"""Return custom report context information."""
|
||||
return {
|
||||
'bom_items': self.get_bom_items(),
|
||||
'bom_items': cast(report.mixins.QuerySet['BomItem'], self.get_bom_items()),
|
||||
'category': self.category,
|
||||
'description': self.description,
|
||||
'IPN': self.IPN,
|
||||
@ -1731,7 +1764,7 @@ class Part(
|
||||
|
||||
return bom_filter
|
||||
|
||||
def get_bom_items(self, include_inherited=True):
|
||||
def get_bom_items(self, include_inherited=True) -> QuerySet[BomItem]:
|
||||
"""Return a queryset containing all BOM items for this part.
|
||||
|
||||
By default, will include inherited BOM items
|
||||
@ -2294,7 +2327,9 @@ class Part(
|
||||
|
||||
parameter.save()
|
||||
|
||||
def getTestTemplates(self, required=None, include_parent=True, enabled=None):
|
||||
def getTestTemplates(
|
||||
self, required=None, include_parent=True, enabled=None
|
||||
) -> QuerySet[PartTestTemplate]:
|
||||
"""Return a list of all test templates associated with this Part.
|
||||
|
||||
These are used for validation of a StockItem.
|
||||
|
@ -69,7 +69,7 @@ class BarcodeMixin:
|
||||
|
||||
return True
|
||||
|
||||
def generate(self, model_instance: InvenTreeBarcodeMixin):
|
||||
def generate(self, model_instance: InvenTreeBarcodeMixin) -> str:
|
||||
"""Generate barcode data for the given model instance.
|
||||
|
||||
Arguments:
|
||||
|
@ -1,7 +1,24 @@
|
||||
"""Report mixin classes."""
|
||||
|
||||
from typing import Generic, TypedDict, TypeVar
|
||||
|
||||
from django.db import models
|
||||
|
||||
_Model = TypeVar('_Model', bound=models.Model, covariant=True)
|
||||
|
||||
|
||||
class QuerySet(Generic[_Model]):
|
||||
"""A custom QuerySet class used for type hinting in report context definitions.
|
||||
|
||||
This will later be replaced by django.db.models.QuerySet, but as
|
||||
django's QuerySet is not a generic class, we need to create our own to not
|
||||
loose type data.
|
||||
"""
|
||||
|
||||
|
||||
class BaseReportContext(TypedDict):
|
||||
"""Base context for a report model."""
|
||||
|
||||
|
||||
class InvenTreeReportMixin(models.Model):
|
||||
"""A mixin class for adding report generation functionality to a model class.
|
||||
@ -15,9 +32,28 @@ class InvenTreeReportMixin(models.Model):
|
||||
|
||||
abstract = True
|
||||
|
||||
def report_context(self) -> dict:
|
||||
def report_context(self) -> BaseReportContext:
|
||||
"""Generate a dict of context data to provide to the reporting framework.
|
||||
|
||||
The default implementation returns an empty dict object.
|
||||
|
||||
This method must contain a type annotation, as it is used by the report generation framework
|
||||
to determine the type of context data that is provided to the report template.
|
||||
|
||||
Example:
|
||||
```python
|
||||
class MyModelReportContext(BaseReportContext):
|
||||
my_field: str
|
||||
po: order.models.PurchaseOrder
|
||||
bom_items: report.mixins.QuerySet[part.models.BomItem]
|
||||
|
||||
class MyModel(report.mixins.InvenTreeReportMixin):
|
||||
...
|
||||
def report_context(self) -> MyModelReportContext:
|
||||
return {
|
||||
'my_field': self.my_field,
|
||||
'po': self.po,
|
||||
}
|
||||
```
|
||||
"""
|
||||
return {}
|
||||
|
@ -3,8 +3,11 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
from typing import Optional, TypedDict, cast
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
@ -123,6 +126,56 @@ class TemplateUploadMixin:
|
||||
return super().validate_unique(exclude)
|
||||
|
||||
|
||||
class BaseContextExtension(TypedDict):
|
||||
"""Base context extension.
|
||||
|
||||
Attributes:
|
||||
base_url: The base URL for the InvenTree instance
|
||||
date: Current date, represented as a Python datetime.date object
|
||||
datetime: Current datetime, represented as a Python datetime object
|
||||
template: The report template instance which is being rendered against
|
||||
template_description: Description of the report template
|
||||
template_name: Name of the report template
|
||||
template_revision: Revision of the report template
|
||||
user: User who made the request to render the template
|
||||
"""
|
||||
|
||||
base_url: str
|
||||
date: date
|
||||
datetime: datetime
|
||||
template: 'ReportTemplateBase'
|
||||
template_description: str
|
||||
template_name: str
|
||||
template_revision: int
|
||||
user: Optional[AbstractUser]
|
||||
|
||||
|
||||
class LabelContextExtension(TypedDict):
|
||||
"""Label report context extension.
|
||||
|
||||
Attributes:
|
||||
width: The width of the label (in mm)
|
||||
height: The height of the label (in mm)
|
||||
page_style: The CSS @page style for the label template. This is used to be inserted at the top of the style block for a given label
|
||||
"""
|
||||
|
||||
width: float
|
||||
height: float
|
||||
page_style: Optional[str]
|
||||
|
||||
|
||||
class ReportContextExtension(TypedDict):
|
||||
"""Report context extension.
|
||||
|
||||
Attributes:
|
||||
page_size: The page size of the report
|
||||
landscape: Boolean value, True if the report is in landscape mode
|
||||
"""
|
||||
|
||||
page_size: str
|
||||
landscape: bool
|
||||
|
||||
|
||||
class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
|
||||
"""Base class for reports, labels."""
|
||||
|
||||
@ -245,7 +298,7 @@ class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
|
||||
"""Return a filter dict which can be applied to the target model."""
|
||||
return report.validators.validate_filters(self.filters, model=self.get_model())
|
||||
|
||||
def base_context(self, request=None):
|
||||
def base_context(self, request=None) -> BaseContextExtension:
|
||||
"""Return base context data (available to all templates)."""
|
||||
return {
|
||||
'base_url': get_base_url(request=request),
|
||||
@ -313,11 +366,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
help_text=_('Render report in landscape orientation'),
|
||||
)
|
||||
|
||||
def get_report_size(self):
|
||||
def get_report_size(self) -> str:
|
||||
"""Return the printable page size for this report."""
|
||||
try:
|
||||
page_size_default = get_global_setting(
|
||||
'REPORT_DEFAULT_PAGE_SIZE', 'A4', create=False
|
||||
page_size_default = cast(
|
||||
str, get_global_setting('REPORT_DEFAULT_PAGE_SIZE', 'A4', create=False)
|
||||
)
|
||||
except Exception:
|
||||
page_size_default = 'A4'
|
||||
@ -331,12 +384,14 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
|
||||
def get_context(self, instance, request=None, **kwargs):
|
||||
"""Supply context data to the report template for rendering."""
|
||||
context = {
|
||||
**super().get_context(instance, request),
|
||||
base_context = super().get_context(instance, request)
|
||||
report_context: ReportContextExtension = {
|
||||
'page_size': self.get_report_size(),
|
||||
'landscape': self.landscape,
|
||||
}
|
||||
|
||||
context = {**base_context, **report_context}
|
||||
|
||||
# Pass the context through to the plugin registry for any additional information
|
||||
for plugin in registry.with_mixin(PluginMixinEnum.REPORT):
|
||||
try:
|
||||
@ -536,12 +591,15 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
||||
|
||||
def get_context(self, instance, request=None, **kwargs):
|
||||
"""Supply context data to the label template for rendering."""
|
||||
context = {
|
||||
**super().get_context(instance, request, **kwargs),
|
||||
base_context = super().get_context(instance, request, **kwargs)
|
||||
label_context: LabelContextExtension = {
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'page_style': None,
|
||||
}
|
||||
|
||||
context = {**base_context, **label_context}
|
||||
|
||||
if kwargs.pop('insert_page_style', True):
|
||||
context['page_style'] = self.generate_page_style()
|
||||
|
||||
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
@ -114,6 +115,24 @@ class StockLocationManager(TreeManager):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class StockLocationReportContext(report.mixins.BaseReportContext):
|
||||
"""Report context for the StockLocation model.
|
||||
|
||||
Attributes:
|
||||
location: The StockLocation object itself
|
||||
qr_data: Formatted QR code data for the StockLocation
|
||||
parent: The parent StockLocation object
|
||||
stock_location: The StockLocation object itself (shadow of 'location')
|
||||
stock_items: Query set of all StockItem objects which are located in the StockLocation
|
||||
"""
|
||||
|
||||
location: StockLocation
|
||||
qr_data: str
|
||||
parent: Optional[StockLocation]
|
||||
stock_location: StockLocation
|
||||
stock_items: report.mixins.QuerySet[StockItem]
|
||||
|
||||
|
||||
class StockLocation(
|
||||
InvenTree.models.InvenTreeBarcodeMixin,
|
||||
report.mixins.InvenTreeReportMixin,
|
||||
@ -159,7 +178,7 @@ class StockLocation(
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return 'SL'
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> StockLocationReportContext:
|
||||
"""Return report context data for this StockLocation."""
|
||||
return {
|
||||
'location': self,
|
||||
@ -338,6 +357,56 @@ def default_delete_on_deplete():
|
||||
return True
|
||||
|
||||
|
||||
class StockItemReportContext(report.mixins.BaseReportContext):
|
||||
"""Report context for the StockItem model.
|
||||
|
||||
Attributes:
|
||||
barcode_data: Generated barcode data for the StockItem
|
||||
barcode_hash: Hash of the barcode data
|
||||
batch: The batch code for the StockItem
|
||||
child_items: Query set of all StockItem objects which are children of this StockItem
|
||||
ipn: The IPN (internal part number) of the associated Part
|
||||
installed_items: Query set of all StockItem objects which are installed in this StockItem
|
||||
item: The StockItem object itself
|
||||
name: The name of the associated Part
|
||||
part: The Part object which is associated with the StockItem
|
||||
qr_data: Generated QR code data for the StockItem
|
||||
qr_url: Generated URL for embedding in a QR code
|
||||
parameters: Dict object containing the parameters associated with the base Part
|
||||
quantity: The quantity of the StockItem
|
||||
result_list: FLattened list of TestResult data associated with the stock item
|
||||
results: Dict object of TestResult data associated with the StockItem
|
||||
serial: The serial number of the StockItem
|
||||
stock_item: The StockItem object itself (shadow of 'item')
|
||||
tests: Dict object of TestResult data associated with the StockItem (shadow of 'results')
|
||||
test_keys: List of test keys associated with the StockItem
|
||||
test_template_list: List of test templates associated with the StockItem
|
||||
test_templates: Dict object of test templates associated with the StockItem
|
||||
"""
|
||||
|
||||
barcode_data: str
|
||||
barcode_hash: str
|
||||
batch: str
|
||||
child_items: report.mixins.QuerySet[StockItem]
|
||||
ipn: Optional[str]
|
||||
installed_items: set[StockItem]
|
||||
item: StockItem
|
||||
name: str
|
||||
part: PartModels.Part
|
||||
qr_data: str
|
||||
qr_url: str
|
||||
parameters: dict[str, str]
|
||||
quantity: Decimal
|
||||
result_list: list[StockItemTestResult]
|
||||
results: dict[str, StockItemTestResult]
|
||||
serial: Optional[str]
|
||||
stock_item: StockItem
|
||||
tests: dict[str, StockItemTestResult]
|
||||
test_keys: list[str]
|
||||
test_template_list: report.mixins.QuerySet[PartModels.PartTestTemplate]
|
||||
test_templates: dict[str, PartModels.PartTestTemplate]
|
||||
|
||||
|
||||
class StockItem(
|
||||
InvenTree.models.InvenTreeAttachmentMixin,
|
||||
InvenTree.models.InvenTreeBarcodeMixin,
|
||||
@ -415,7 +484,7 @@ class StockItem(
|
||||
|
||||
return list(keys)
|
||||
|
||||
def report_context(self):
|
||||
def report_context(self) -> StockItemReportContext:
|
||||
"""Generate custom report context data for this StockItem."""
|
||||
return {
|
||||
'barcode_data': self.barcode_data,
|
||||
|
Reference in New Issue
Block a user