mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-28 01:17:21 +00:00
[docs] Report discovery (#12449)
* Adds decorator to mark an attribute for report context discovery * Update context variables in docs * Include fields captured against each model type * Expose more properties * Image URL tags * Add more attributes * Rearrange docs * Simplify docs * Enforce type hinting for reportable properties * Expose more properties for the Order model * Auto-discover related / linked models * Remove extra text * Collapsible blocks * Reduce boilerplate * Remove cruft * Enhance documentation for filename generation * Cleanup
This commit is contained in:
@@ -31,7 +31,7 @@ The barcode is tested as follows, in decreasing order of priority:
|
|||||||
|
|
||||||
InvenTree comes with a few builtin supplier plugins, which handle their respective barcode formats.
|
InvenTree comes with a few builtin supplier plugins, which handle their respective barcode formats.
|
||||||
|
|
||||||
Scanning a supplier barcode for a supplied part will link to the corresponding supplier part if the [SKU](../report/context_variables.md#supplierpart) from the barcode could be matched.
|
Scanning a supplier barcode for a supplied part will link to the corresponding supplier part if the [SKU](../report/model_context.md#supplier-part) from the barcode could be matched.
|
||||||
|
|
||||||
The following suppliers (and barcode formats) are currently supported:
|
The following suppliers (and barcode formats) are currently supported:
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: Context Variables
|
title: Report Context
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## Context Variables
|
## Report Context Variables
|
||||||
|
|
||||||
Context variables are provided to each template when it is rendered. The available context variables depend on the model type for which the template is being rendered.
|
Context variables are provided to each template when it is rendered. The available context variables depend on the model type for which the template is being rendered.
|
||||||
|
|
||||||
@@ -13,9 +13,6 @@ In addition to the model-specific context variables, the following global contex
|
|||||||
|
|
||||||
{{ report_context("base", "global") }}
|
{{ report_context("base", "global") }}
|
||||||
|
|
||||||
::: report.models.ReportTemplateBase.base_context
|
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Report Context
|
### Report Context
|
||||||
|
|
||||||
@@ -23,7 +20,7 @@ In addition to the [global context](#global-context), all *report* templates hav
|
|||||||
|
|
||||||
{{ report_context("base", "report") }}
|
{{ report_context("base", "report") }}
|
||||||
|
|
||||||
When using the `merge` context variable, the selected items are available in the `instances` list. {{ templatefile("report/inventree_stock_report_merge.html") }} shows a complete example. To access individual item attributes, you can either loop through the `instances` or access them by index like `instance.0.name`.
|
When using the `merge` context variable, the selected items are available in the `instances` list. To access individual item attributes, you can either loop through the `instances` or access them by index like `instance.0.name`.
|
||||||
|
|
||||||
Below is an example template that generates a single report for some selected parts. Each part occupies a row in the table
|
Below is an example template that generates a single report for some selected parts. Each part occupies a row in the table
|
||||||
|
|
||||||
@@ -87,9 +84,8 @@ When printing a report or label against a Company instance, the following contex
|
|||||||
|
|
||||||
{{ report_context("models", "company") }}
|
{{ report_context("models", "company") }}
|
||||||
|
|
||||||
::: company.models.Company.report_context
|
For the fields and properties available on the underlying `Company` instance, refer to the [Model Context](model_context.md#company) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Build Order
|
### Build Order
|
||||||
|
|
||||||
@@ -97,9 +93,8 @@ When printing a report or label against a [Build Order](../manufacturing/build.m
|
|||||||
|
|
||||||
{{ report_context("models", "build") }}
|
{{ report_context("models", "build") }}
|
||||||
|
|
||||||
::: build.models.Build.report_context
|
For the fields and properties available on the underlying `Build` instance, refer to the [Model Context](model_context.md#build-order) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Build Line
|
### Build Line
|
||||||
|
|
||||||
@@ -107,9 +102,7 @@ When printing a report or label against a [BuildOrderLineItem](../manufacturing/
|
|||||||
|
|
||||||
{{ report_context("models", "buildline") }}
|
{{ report_context("models", "buildline") }}
|
||||||
|
|
||||||
::: build.models.BuildLine.report_context
|
For the fields and properties available on the underlying `BuildLine` instance, refer to the [Model Context](model_context.md#build-order-line-item) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Sales Order
|
### Sales Order
|
||||||
|
|
||||||
@@ -117,9 +110,7 @@ When printing a report or label against a [SalesOrder](../sales/sales_order.md)
|
|||||||
|
|
||||||
{{ report_context("models", "salesorder") }}
|
{{ report_context("models", "salesorder") }}
|
||||||
|
|
||||||
::: order.models.Order.report_context
|
For the fields and properties available on the underlying `SalesOrder` instance, refer to the [Model Context](model_context.md#sales-order) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Sales Order Shipment
|
### Sales Order Shipment
|
||||||
|
|
||||||
@@ -127,9 +118,7 @@ When printing a report or label against a [SalesOrderShipment](../sales/sales_or
|
|||||||
|
|
||||||
{{ report_context("models", "salesordershipment") }}
|
{{ report_context("models", "salesordershipment") }}
|
||||||
|
|
||||||
::: order.models.SalesOrderShipment.report_context
|
For the fields and properties available on the underlying `SalesOrderShipment` instance, refer to the [Model Context](model_context.md#sales-order-shipment) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Return Order
|
### Return Order
|
||||||
|
|
||||||
@@ -137,21 +126,23 @@ When printing a report or label against a [ReturnOrder](../sales/return_order.md
|
|||||||
|
|
||||||
{{ report_context("models", "returnorder") }}
|
{{ report_context("models", "returnorder") }}
|
||||||
|
|
||||||
|
For the fields and properties available on the underlying `ReturnOrder` instance, refer to the [Model Context](model_context.md#return-order) page.
|
||||||
|
|
||||||
### Purchase Order
|
### Purchase Order
|
||||||
|
|
||||||
When printing a report or label against a [PurchaseOrder](../purchasing/purchase_order.md) object, the following context variables are available:
|
When printing a report or label against a [PurchaseOrder](../purchasing/purchase_order.md) object, the following context variables are available:
|
||||||
|
|
||||||
{{ report_context("models", "purchaseorder") }}
|
{{ report_context("models", "purchaseorder") }}
|
||||||
|
|
||||||
|
For the fields and properties available on the underlying `PurchaseOrder` instance, refer to the [Model Context](model_context.md#purchase-order) page.
|
||||||
|
|
||||||
### Transfer Order
|
### Transfer Order
|
||||||
|
|
||||||
When printing a report or label against a [TransferOrder](../stock/transfer_order.md) object, the following context variables are available:
|
When printing a report or label against a [TransferOrder](../stock/transfer_order.md) object, the following context variables are available:
|
||||||
|
|
||||||
{{ report_context("models", "transferorder") }}
|
{{ report_context("models", "transferorder") }}
|
||||||
|
|
||||||
::: order.models.TransferOrder.report_context
|
For the fields and properties available on the underlying `TransferOrder` instance, refer to the [Model Context](model_context.md#transfer-order) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Stock Item
|
### Stock Item
|
||||||
|
|
||||||
@@ -159,9 +150,7 @@ When printing a report or label against a [StockItem](../stock/index.md#stock-it
|
|||||||
|
|
||||||
{{ report_context("models", "stockitem") }}
|
{{ report_context("models", "stockitem") }}
|
||||||
|
|
||||||
::: stock.models.StockItem.report_context
|
For the fields and properties available on the underlying `StockItem` instance, refer to the [Model Context](model_context.md#stock-item) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Stock Location
|
### Stock Location
|
||||||
|
|
||||||
@@ -169,9 +158,7 @@ When printing a report or label against a [StockLocation](../stock/index.md#stoc
|
|||||||
|
|
||||||
{{ report_context("models", "stocklocation") }}
|
{{ report_context("models", "stocklocation") }}
|
||||||
|
|
||||||
::: stock.models.StockLocation.report_context
|
For the fields and properties available on the underlying `StockLocation` instance, refer to the [Model Context](model_context.md#stock-location) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
### Part
|
### Part
|
||||||
|
|
||||||
@@ -179,190 +166,9 @@ When printing a report or label against a [Part](../part/index.md) object, the f
|
|||||||
|
|
||||||
{{ report_context("models", "part") }}
|
{{ report_context("models", "part") }}
|
||||||
|
|
||||||
::: part.models.Part.report_context
|
For the fields and properties available on the underlying `Part` instance, refer to the [Model Context](model_context.md#part) page.
|
||||||
options:
|
|
||||||
show_source: True
|
|
||||||
|
|
||||||
## Model Variables
|
|
||||||
|
|
||||||
Additional to the context variables provided directly to each template, each model type has a number of attributes and methods which can be accessed via the template.
|
|
||||||
|
|
||||||
For each model type, a subset of the most commonly used attributes are listed below. For a full list of attributes and methods, refer to the source code for the particular model type.
|
|
||||||
|
|
||||||
### Parts
|
|
||||||
|
|
||||||
#### Part
|
|
||||||
|
|
||||||
Each part object has access to a lot of context variables about the part. The following context variables are provided when accessing a `Part` object from within the template.
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| name | Brief name for this part |
|
|
||||||
| full_name | Full name for this part (including IPN, if not null and including variant, if not null) |
|
|
||||||
| category | The [PartCategory](#part-category) object to which this part belongs
|
|
||||||
| description | Longer form description of the part
|
|
||||||
| keywords | Optional keywords for improving part search results
|
|
||||||
| IPN | Internal part number (optional)
|
|
||||||
| revision | Part revision
|
|
||||||
| is_template | If True, this part is a 'template' part
|
|
||||||
| link | Link to an external page with more information about this part (e.g. internal Wiki)
|
|
||||||
| image | Image of this part
|
|
||||||
| default_location | The default [StockLocation](#stock-location) object where the item is normally stored (may be null)
|
|
||||||
| default_supplier | The default [SupplierPart](#supplierpart) which should be used to procure and stock this part
|
|
||||||
| default_expiry | The default expiry duration for any StockItem instances of this part
|
|
||||||
| minimum_stock | Minimum preferred quantity to keep in stock
|
|
||||||
| units | Units of measure for this part (default='pcs')
|
|
||||||
| salable | Can this part be sold to customers?
|
|
||||||
| assembly | Can this part be build from other parts?
|
|
||||||
| component | Can this part be used to make other parts?
|
|
||||||
| purchaseable | Can this part be purchased from suppliers?
|
|
||||||
| trackable | Trackable parts can have unique serial numbers assigned, etc, etc
|
|
||||||
| active | Is this part active? Parts are deactivated instead of being deleted
|
|
||||||
| virtual | Is this part "virtual"? e.g. a software product or similar
|
|
||||||
| notes | Additional notes field for this part
|
|
||||||
| creation_date | Date that this part was added to the database
|
|
||||||
| creation_user | User who added this part to the database
|
|
||||||
| responsible | User who is responsible for this part (optional)
|
|
||||||
| starred | Whether the part is starred or not |
|
|
||||||
| disabled | Whether the part is disabled or not |
|
|
||||||
| total_stock | The total amount in stock |
|
|
||||||
| quantity_being_built | The amount being built |
|
|
||||||
| required_build_order_quantity | The amount required for build orders |
|
|
||||||
| allocated_build_order_quantity | The amount allocated for build orders |
|
|
||||||
| build_order_allocations | Query set with all build order allocations for that part |
|
|
||||||
| required_sales_order_quantity | The amount required for sales orders |
|
|
||||||
| allocated_sales_order_quantity | The amount allocated for sales orders |
|
|
||||||
| available | Whether the part is available or not |
|
|
||||||
| on_order | The amount that are on order |
|
|
||||||
| required | The total amount required for build orders and sales orders |
|
|
||||||
| allocated | The total amount allocated for build orders and sales orders |
|
|
||||||
|
|
||||||
#### Part Category
|
|
||||||
|
|
||||||
|
|
||||||
| Variable | Description |
|
## Model Context
|
||||||
|----------|-------------|
|
|
||||||
| name | Name of this category |
|
|
||||||
| parent | Parent category |
|
|
||||||
| default_location | Default [StockLocation](#stock-location) object for parts in this category or child categories |
|
|
||||||
| default_keywords | Default keywords for parts created in this category |
|
|
||||||
|
|
||||||
### Stock
|
For the underlying fields and properties available on each of the model instances above (plus related model types such as `Address` or `SupplierPart`), refer to the [Model Context](model_context.md) page.
|
||||||
|
|
||||||
#### StockItem
|
|
||||||
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| parent | Link to another [StockItem](#stock-item) from which this StockItem was created |
|
|
||||||
| part | Link to the master abstract [Part](#part) that this [StockItem](#stock-item) is an instance of |
|
|
||||||
| supplier_part | Link to a specific [SupplierPart](#supplierpart) (optional) |
|
|
||||||
| location | The [StockLocation](#stock-location) Where this [StockItem](#stock-item) is located |
|
|
||||||
| quantity | Number of stocked units |
|
|
||||||
| batch | Batch number for this [StockItem](#stock-item) |
|
|
||||||
| serial | Unique serial number for this [StockItem](#stock-item) |
|
|
||||||
| link | Optional URL to link to external resource |
|
|
||||||
| updated | Date that this stock item was last updated (auto) |
|
|
||||||
| expiry_date | Expiry date of the [StockItem](#stock-item) (optional) |
|
|
||||||
| stocktake_date | Date of last stocktake for this item |
|
|
||||||
| stocktake_user | User that performed the most recent stocktake |
|
|
||||||
| delete_on_deplete | If True, [StockItem](#stock-item) will be deleted when the stock level gets to zero |
|
|
||||||
| status | Status of this [StockItem](#stock-item) (ref: InvenTree.status_codes.StockStatus) |
|
|
||||||
| status_label | Textual representation of the status e.g. "OK" |
|
|
||||||
| notes | Extra notes field |
|
|
||||||
| build | Link to a Build (if this stock item was created from a build) |
|
|
||||||
| is_building | Boolean field indicating if this stock item is currently being built (or is "in production") |
|
|
||||||
| purchase_order | Link to a [PurchaseOrder](#purchase-order) (if this stock item was created from a PurchaseOrder) |
|
|
||||||
| sales_order | Link to a [SalesOrder](#sales-order) object (if the StockItem has been assigned to a SalesOrder) |
|
|
||||||
| purchase_price | The unit purchase price for this [StockItem](#stock-item) - this is the unit price at time of purchase (if this item was purchased from an external supplier) |
|
|
||||||
| packaging | Description of how the StockItem is packaged (e.g. "reel", "loose", "tape" etc) |
|
|
||||||
|
|
||||||
#### StockLocation
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| barcode | Brief payload data (e.g. for labels). Example: `{"stocklocation": 826}` where 826 is the primary key|
|
|
||||||
| description | The description of the location |
|
|
||||||
| icon | The name of the icon if set, e.g. fas fa-warehouse |
|
|
||||||
| item_count | Simply returns the number of stock items in this location |
|
|
||||||
| name | The name of the location. This is only the name of this location, not the path |
|
|
||||||
| owner | The owner of the location if it has one |
|
|
||||||
| parent | The parent location. Returns None if it is already the top most one |
|
|
||||||
| path | A queryset of locations that contains the hierarchy starting from the top most parent |
|
|
||||||
| pathstring | A string that contains all names of the path separated by slashes e.g. A/B/C |
|
|
||||||
| structural | True if the location is structural |
|
|
||||||
|
|
||||||
### Suppliers
|
|
||||||
|
|
||||||
#### Company
|
|
||||||
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| name | Name of the company |
|
|
||||||
| description | Longer form description |
|
|
||||||
| website | URL for the company website |
|
|
||||||
| primary_address | [Address](#address) object that is marked as primary address |
|
|
||||||
| address | String format of the primary address |
|
|
||||||
| contact | Contact Name |
|
|
||||||
| phone | Contact phone number |
|
|
||||||
| email | Contact email address |
|
|
||||||
| link | URL associated with the company |
|
|
||||||
| notes | Extra notes about the company |
|
|
||||||
| is_customer | Boolean value, is this company a customer |
|
|
||||||
| is_supplier | Boolean value, is this company a supplier |
|
|
||||||
| is_manufacturer | Boolean value, is this company a manufacturer |
|
|
||||||
| currency_code | Default currency for the company |
|
|
||||||
| parts | Query set with all parts that the company supplies |
|
|
||||||
|
|
||||||
#### Address
|
|
||||||
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| line1 | First line of the postal address |
|
|
||||||
| line2 | Second line of the postal address |
|
|
||||||
| postal_code | ZIP code of the city |
|
|
||||||
| postal_city | City name |
|
|
||||||
| country | Country name |
|
|
||||||
|
|
||||||
#### Contact
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| company | Company object where the contact belongs to |
|
|
||||||
| name | First and second name of the contact |
|
|
||||||
| phone | Phone number |
|
|
||||||
| email | Email address |
|
|
||||||
| role | Role of the contact |
|
|
||||||
|
|
||||||
#### SupplierPart
|
|
||||||
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| part | Link to the master Part (Obsolete) |
|
|
||||||
| source_item | The sourcing [StockItem](#stock-item) linked to this [SupplierPart](#supplierpart) instance |
|
|
||||||
| supplier | [Company](#company) that supplies this part |
|
|
||||||
| SKU | Stock keeping unit (supplier part number) |
|
|
||||||
| link | Link to external website for this supplier part |
|
|
||||||
| description | Descriptive notes field |
|
|
||||||
| note | Longer form note field |
|
|
||||||
| base_cost | Base charge added to order independent of quantity e.g. "Reeling Fee" |
|
|
||||||
| multiple | Multiple that the part is provided in |
|
|
||||||
| packaging | packaging that the part is supplied in, e.g. "Reel" |
|
|
||||||
| pretty_name | The IPN, supplier name, supplier SKU and (if not null) manufacturer string joined by `|`. Ex. `P00037 | Company | 000021` |
|
|
||||||
| unit_pricing | The price for one unit. |
|
|
||||||
| price_breaks | Return the associated price breaks in the correct order |
|
|
||||||
| has_price_breaks | Whether this [SupplierPart](#supplierpart) has price breaks |
|
|
||||||
| manufacturer_string | Format a MPN string for this [SupplierPart](#supplierpart). Concatenates manufacture name and part number. |
|
|
||||||
|
|
||||||
### User
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| username | the username of the user |
|
|
||||||
| first_name | The first name of the user |
|
|
||||||
| last_name | The last name of the user |
|
|
||||||
| email | The email address of the user |
|
|
||||||
| pk | The primary key of the user |
|
|
||||||
|
|||||||
@@ -94,10 +94,23 @@ If the *Attach to Model on Print* option is enabled, a copy of the generated rep
|
|||||||
|
|
||||||
The filename pattern used to generate the output `.pdf` file. Defaults to "report.pdf".
|
The filename pattern used to generate the output `.pdf` file. Defaults to "report.pdf".
|
||||||
|
|
||||||
The filename pattern allows custom rendering with any context variables which are available to the report. For example, a test report for a particular [Stock Item](../stock/index.md#stock-item) can use the part name and serial number of the stock item when generating the report name:
|
The filename pattern is rendered using the same Django template syntax (`{% raw %}{{ ... }}{% endraw %}`) as the report body, using the same context data - see the [context variables](./context_variables.md) and [model context](./model_context.md) documentation for the full list of variables available to a particular template type.
|
||||||
|
|
||||||
|
!!! info "Filename Pattern Context"
|
||||||
|
The filename pattern has access to the *entire* context available to the report or label being generated - not a reduced subset.
|
||||||
|
|
||||||
|
For example, a test report for a particular [Stock Item](../stock/index.md#stock-item) can use the part name and serial number of the stock item when generating the report name:
|
||||||
|
|
||||||
{{ image("report/filename_pattern.png", "Report filename pattern") }}
|
{{ image("report/filename_pattern.png", "Report filename pattern") }}
|
||||||
|
|
||||||
|
Some further examples:
|
||||||
|
|
||||||
|
| Filename Pattern | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `{% raw %}{{ part.full_name }}-{{ serial }}.pdf{% endraw %}` | Combines the [Part](./model_context.md#part) name with the stock item serial number |
|
||||||
|
| `{% raw %}PurchaseOrder-{{ reference }}.pdf{% endraw %}` | Uses the purchase order reference (this is the default pattern for the built-in Purchase Order report) |
|
||||||
|
| `{% raw %}SalesOrder-{{ reference }}-{{ date }}.pdf{% endraw %}` | Combines the sales order reference with the current date |
|
||||||
|
|
||||||
|
|
||||||
### Template Filters
|
### Template Filters
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: Model Context
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model Context Variables
|
||||||
|
|
||||||
|
Each [report context](./context_variables.md) exposes one or more underlying model instances to the template (e.g. `order`, `part`, `item`). In addition to the explicit context variables documented on that page, templates can access any field or property of these model instances directly.
|
||||||
|
|
||||||
|
This page documents, for each reportable model type, the available:
|
||||||
|
|
||||||
|
- **Fields**: Database fields defined on the model (including those inherited from mixins / abstract base classes)
|
||||||
|
- **Properties**: `@property` attributes which have been explicitly marked with the `@report_attribute` decorator, making them discoverable here
|
||||||
|
|
||||||
|
Not every attribute or method available on a model instance is listed here - only database fields, and properties explicitly marked for discovery. For a full list of attributes and methods, refer to the source code for the particular model type.
|
||||||
|
|
||||||
|
{{ reportable_model_context() }}
|
||||||
|
|
||||||
|
## Related Model Types
|
||||||
|
|
||||||
|
Some *related* model types are not themselves directly reportable, but are referenced by a field or `@report_attribute` property on a reportable model (e.g. `PartCategory` via `part.category`, `SupplierPart` via `part.default_supplier` or `stockitem.supplier_part`).
|
||||||
|
|
||||||
|
These are discovered automatically - there is no manually-maintained list of related models here. If a reportable model gains a new field or `@report_attribute` property pointing at another model, that model will automatically appear in this section.
|
||||||
|
|
||||||
|
{{ related_model_context() }}
|
||||||
@@ -442,6 +442,75 @@ def define_env(env):
|
|||||||
|
|
||||||
return ret_data
|
return ret_data
|
||||||
|
|
||||||
|
def render_attribute_table(attributes: dict, title: str) -> str:
|
||||||
|
"""Render a table of field/property information, in a collapsible (collapsed by default) block.
|
||||||
|
|
||||||
|
Returns an empty string (including no block) if there is nothing to display.
|
||||||
|
"""
|
||||||
|
if not attributes:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
table = '| Variable | Type | Description |\n| --- | --- | --- |\n'
|
||||||
|
|
||||||
|
for k, v in sorted(attributes.items()):
|
||||||
|
description = ' '.join(v['description'].split())
|
||||||
|
table += f'| {k} | `{v["type"]}` | {description} |\n'
|
||||||
|
|
||||||
|
ret_data = f'??? note "{title}"\n\n'
|
||||||
|
ret_data += textwrap.indent(table, ' ')
|
||||||
|
|
||||||
|
# Trailing blank line, so consecutive macro calls (e.g. fields followed by
|
||||||
|
# properties) don't run together when the template places them on adjacent lines
|
||||||
|
return ret_data + '\n'
|
||||||
|
|
||||||
|
@env.macro
|
||||||
|
def reportable_model_context():
|
||||||
|
"""Render the full 'reportable model types' section.
|
||||||
|
|
||||||
|
One heading plus fields/properties tables per model which templates can be
|
||||||
|
rendered against (e.g. `Part`, `SalesOrder`, `StockItem`). The list of models
|
||||||
|
comes directly from what `export_report_context` discovered via the
|
||||||
|
`InvenTreeReportMixin`, so there is no manually-maintained per-model heading
|
||||||
|
list to keep in sync here.
|
||||||
|
"""
|
||||||
|
global REPORT_CONTEXT
|
||||||
|
|
||||||
|
models = REPORT_CONTEXT.get('models', {})
|
||||||
|
|
||||||
|
ret_data = ''
|
||||||
|
|
||||||
|
for info in sorted(models.values(), key=lambda item: item['name']):
|
||||||
|
ret_data += f'### {info["name"]}\n\n'
|
||||||
|
ret_data += render_attribute_table(info.get('fields', {}), 'Fields')
|
||||||
|
ret_data += render_attribute_table(info.get('properties', {}), 'Properties')
|
||||||
|
|
||||||
|
return ret_data
|
||||||
|
|
||||||
|
@env.macro
|
||||||
|
def related_model_context():
|
||||||
|
"""Render the full 'related model types' section.
|
||||||
|
|
||||||
|
A "related" model is one which is not itself reportable, but which is
|
||||||
|
referenced by a field or `@report_attribute` property on a reportable model
|
||||||
|
(e.g. `PartCategory` via `Part.category`, `SupplierPart` via `Part.default_supplier`).
|
||||||
|
|
||||||
|
These are discovered automatically (one hop out from the reportable models) by
|
||||||
|
the `export_report_context` management command, so there is no manually-maintained
|
||||||
|
list of related models to keep in sync here.
|
||||||
|
"""
|
||||||
|
global REPORT_CONTEXT
|
||||||
|
|
||||||
|
related = REPORT_CONTEXT.get('related_models', {})
|
||||||
|
|
||||||
|
ret_data = ''
|
||||||
|
|
||||||
|
for info in sorted(related.values(), key=lambda item: item['name']):
|
||||||
|
ret_data += f'### {info["name"]}\n\n'
|
||||||
|
ret_data += render_attribute_table(info.get('fields', {}), 'Fields')
|
||||||
|
ret_data += render_attribute_table(info.get('properties', {}), 'Properties')
|
||||||
|
|
||||||
|
return ret_data
|
||||||
|
|
||||||
@env.macro
|
@env.macro
|
||||||
def icon(
|
def icon(
|
||||||
source: str,
|
source: str,
|
||||||
|
|||||||
+2
-1
@@ -183,7 +183,8 @@ nav:
|
|||||||
- Labels: report/labels.md
|
- Labels: report/labels.md
|
||||||
- Report Assets: report/assets.md
|
- Report Assets: report/assets.md
|
||||||
- Report Snippets: report/snippets.md
|
- Report Snippets: report/snippets.md
|
||||||
- Context Variables: report/context_variables.md
|
- Report Context: report/context_variables.md
|
||||||
|
- Model Context: report/model_context.md
|
||||||
- Helper Functions: report/helpers.md
|
- Helper Functions: report/helpers.md
|
||||||
- Barcodes: report/barcodes.md
|
- Barcodes: report/barcodes.md
|
||||||
- Sample Templates: report/samples.md
|
- Sample Templates: report/samples.md
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ This in turn allows report context to be documented in the InvenTree documentati
|
|||||||
without having to manually duplicate the information in multiple places.
|
without having to manually duplicate the information in multiple places.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
from typing import get_args, get_origin, get_type_hints
|
from typing import get_args, get_origin, get_type_hints
|
||||||
|
|
||||||
import django.db.models
|
import django.db.models
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
from report.mixins import QuerySet
|
from report.mixins import REPORT_ATTRIBUTE_MARKER, QuerySet
|
||||||
|
|
||||||
|
|
||||||
def get_type_str(type_obj):
|
def get_type_str(type_obj):
|
||||||
@@ -38,6 +39,29 @@ def get_type_str(type_obj):
|
|||||||
return f'{type_obj.__module__}.{type_obj.__name__}'
|
return f'{type_obj.__module__}.{type_obj.__name__}'
|
||||||
|
|
||||||
|
|
||||||
|
def find_related_models(type_obj) -> set:
|
||||||
|
"""Recursively find any Django Model subclasses referenced by a type annotation.
|
||||||
|
|
||||||
|
Handles plain model classes, `Optional[Model]`, and `report.mixins.QuerySet[Model]`
|
||||||
|
(and any nesting thereof), so that models referenced from a field/property type
|
||||||
|
can be discovered for the "related model" documentation, without needing to be
|
||||||
|
manually curated.
|
||||||
|
"""
|
||||||
|
found = set()
|
||||||
|
|
||||||
|
if type_obj is None or type_obj is type(None):
|
||||||
|
return found
|
||||||
|
|
||||||
|
if inspect.isclass(type_obj) and issubclass(type_obj, django.db.models.Model):
|
||||||
|
found.add(type_obj)
|
||||||
|
return found
|
||||||
|
|
||||||
|
for arg in get_args(type_obj):
|
||||||
|
found |= find_related_models(arg)
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
def parse_docstring(docstring: str):
|
def parse_docstring(docstring: str):
|
||||||
"""Parse the docstring of a type object and return a dictionary of sections."""
|
"""Parse the docstring of a type object and return a dictionary of sections."""
|
||||||
sections = {}
|
sections = {}
|
||||||
@@ -59,6 +83,92 @@ def parse_docstring(docstring: str):
|
|||||||
return sections
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def get_report_attributes(model):
|
||||||
|
"""Find all properties/methods on a model marked with @report_attribute.
|
||||||
|
|
||||||
|
This walks the full MRO of the model class, so attributes contributed by
|
||||||
|
mixins (e.g. `InvenTreeBarcodeMixin.barcode`) are discovered too, regardless
|
||||||
|
of whether they are also explicitly included in `report_context()`.
|
||||||
|
|
||||||
|
Returns a tuple of:
|
||||||
|
|
||||||
|
- A dict of {name: {"description": ..., "type": ...}}, ordered so that
|
||||||
|
attributes redefined further down the MRO (i.e. on the model itself) take
|
||||||
|
precedence over the same name defined on a base/mixin class.
|
||||||
|
- A list of qualified names (e.g. "Part.default_supplier") for any
|
||||||
|
@report_attribute-marked property/method which is missing a return type
|
||||||
|
annotation.
|
||||||
|
- A set of any Django Model subclasses referenced by the return type of a
|
||||||
|
discovered property (e.g. `SupplierPart` for `Part.default_supplier`), used
|
||||||
|
to auto-discover "related" models which are not themselves reportable.
|
||||||
|
"""
|
||||||
|
found = {}
|
||||||
|
errors = []
|
||||||
|
related_models = set()
|
||||||
|
|
||||||
|
for klass in reversed(model.__mro__):
|
||||||
|
for name, member in vars(klass).items():
|
||||||
|
target = member.fget if isinstance(member, property) else member
|
||||||
|
description = getattr(target, REPORT_ATTRIBUTE_MARKER, None)
|
||||||
|
|
||||||
|
if description is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return_type = get_type_hints(target).get('return', None)
|
||||||
|
|
||||||
|
if return_type is None:
|
||||||
|
errors.append(f'{klass.__name__}.{name}')
|
||||||
|
else:
|
||||||
|
related_models |= find_related_models(return_type)
|
||||||
|
|
||||||
|
found[name] = {
|
||||||
|
'description': description,
|
||||||
|
'type': get_type_str(return_type) if return_type is not None else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
return found, errors, related_models
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_field_attributes(model):
|
||||||
|
"""Find all concrete database fields defined on a model.
|
||||||
|
|
||||||
|
This includes fields inherited from mixins/abstract base classes, so (for
|
||||||
|
example) `barcode_data` and `barcode_hash` are discovered for any model
|
||||||
|
which uses `InvenTreeBarcodeMixin`, without needing to be documented by hand.
|
||||||
|
|
||||||
|
Reverse relations (e.g. the 'lines' accessor on a linked order) are excluded,
|
||||||
|
as they are not actual fields on this model's table but related querysets -
|
||||||
|
these are documented (where relevant) via `report_context()` or `@report_attribute` instead.
|
||||||
|
|
||||||
|
Returns a tuple of:
|
||||||
|
|
||||||
|
- A dict of {name: {"description": ..., "type": ...}}.
|
||||||
|
- A set of any Django Model subclasses referenced by a relation field (e.g.
|
||||||
|
`PartCategory` for `Part.category`), used to auto-discover "related" models
|
||||||
|
which are not themselves reportable.
|
||||||
|
"""
|
||||||
|
found = {}
|
||||||
|
related_models = set()
|
||||||
|
|
||||||
|
for field in model._meta.get_fields():
|
||||||
|
if not field.concrete:
|
||||||
|
continue
|
||||||
|
|
||||||
|
description = str(getattr(field, 'help_text', '') or '') or str(
|
||||||
|
getattr(field, 'verbose_name', field.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
type_str = type(field).__name__
|
||||||
|
|
||||||
|
if field.is_relation and field.related_model is not None:
|
||||||
|
type_str = f'{type_str}[{get_type_str(field.related_model)}]'
|
||||||
|
related_models.add(field.related_model)
|
||||||
|
|
||||||
|
found[field.name] = {'description': description, 'type': type_str}
|
||||||
|
|
||||||
|
return found, related_models
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Extract report context information, and export to a JSON file."""
|
"""Extract report context information, and export to a JSON file."""
|
||||||
|
|
||||||
@@ -79,8 +189,10 @@ class Command(BaseCommand):
|
|||||||
ReportContextExtension,
|
ReportContextExtension,
|
||||||
)
|
)
|
||||||
|
|
||||||
context = {'models': {}, 'base': {}}
|
context = {'models': {}, 'related_models': {}, 'base': {}}
|
||||||
is_error = False
|
is_error = False
|
||||||
|
is_attribute_error = False
|
||||||
|
related_model_classes = set()
|
||||||
|
|
||||||
# Base context models
|
# Base context models
|
||||||
for key, model in [
|
for key, model in [
|
||||||
@@ -111,10 +223,23 @@ class Command(BaseCommand):
|
|||||||
is_error = True
|
is_error = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
fields, field_related = get_model_field_attributes(model)
|
||||||
|
properties, property_errors, property_related = get_report_attributes(model)
|
||||||
|
|
||||||
|
related_model_classes |= field_related | property_related
|
||||||
|
|
||||||
|
for qualified_name in property_errors:
|
||||||
|
print(
|
||||||
|
f'Error: {qualified_name} is marked with @report_attribute but does not have a return type annotation'
|
||||||
|
)
|
||||||
|
is_attribute_error = True
|
||||||
|
|
||||||
context['models'][model_key] = {
|
context['models'][model_key] = {
|
||||||
'key': model_key,
|
'key': model_key,
|
||||||
'name': model_name,
|
'name': model_name,
|
||||||
'context': {},
|
'context': {},
|
||||||
|
'fields': fields,
|
||||||
|
'properties': properties,
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes = parse_docstring(ctx_type.__doc__).get('Attributes', {})
|
attributes = parse_docstring(ctx_type.__doc__).get('Attributes', {})
|
||||||
@@ -129,6 +254,38 @@ class Command(BaseCommand):
|
|||||||
'INVE-E4 - Some models associated with the `InvenTreeReportMixin` do not have a valid `report_context` return type annotation.'
|
'INVE-E4 - Some models associated with the `InvenTreeReportMixin` do not have a valid `report_context` return type annotation.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Related models: models which are not themselves reportable, but which are
|
||||||
|
# referenced by a field or @report_attribute property on a reportable model.
|
||||||
|
# Discovered automatically (one hop) from the fields/properties collected above,
|
||||||
|
# so no manual list of "related" models needs to be maintained.
|
||||||
|
reportable_models = set(report_model_types())
|
||||||
|
|
||||||
|
for model in sorted(
|
||||||
|
related_model_classes - reportable_models, key=lambda m: m.__name__
|
||||||
|
):
|
||||||
|
model_key = model.__name__.lower()
|
||||||
|
|
||||||
|
fields, _ = get_model_field_attributes(model)
|
||||||
|
properties, property_errors, _ = get_report_attributes(model)
|
||||||
|
|
||||||
|
for qualified_name in property_errors:
|
||||||
|
print(
|
||||||
|
f'Error: {qualified_name} is marked with @report_attribute but does not have a return type annotation'
|
||||||
|
)
|
||||||
|
is_attribute_error = True
|
||||||
|
|
||||||
|
context['related_models'][model_key] = {
|
||||||
|
'key': model_key,
|
||||||
|
'name': str(model._meta.verbose_name).title(),
|
||||||
|
'fields': fields,
|
||||||
|
'properties': properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_attribute_error:
|
||||||
|
raise CommandError(
|
||||||
|
'INVE-E5 - Some properties/methods marked with `@report_attribute` do not have a valid return type annotation.'
|
||||||
|
)
|
||||||
|
|
||||||
filename = kwargs.get('filename', 'inventree_report_context.json')
|
filename = kwargs.get('filename', 'inventree_report_context.json')
|
||||||
|
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import InvenTree.format
|
|||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.helpers_model
|
import InvenTree.helpers_model
|
||||||
import InvenTree.sentry
|
import InvenTree.sentry
|
||||||
|
import report.mixins
|
||||||
|
|
||||||
logger = structlog.get_logger('inventree')
|
logger = structlog.get_logger('inventree')
|
||||||
|
|
||||||
@@ -559,7 +560,8 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parameters(self) -> QuerySet:
|
@report.mixins.report_attribute()
|
||||||
|
def parameters(self) -> report.mixins.QuerySet:
|
||||||
"""Return a QuerySet containing all the Parameter instances for this model.
|
"""Return a QuerySet containing all the Parameter instances for this model.
|
||||||
|
|
||||||
This will return pre-fetched data if available (i.e. in a serializer context).
|
This will return pre-fetched data if available (i.e. in a serializer context).
|
||||||
@@ -692,7 +694,8 @@ class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin):
|
|||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attachments(self) -> QuerySet:
|
@report.mixins.report_attribute()
|
||||||
|
def attachments(self) -> report.mixins.QuerySet:
|
||||||
"""Return a queryset containing all attachments for this model."""
|
"""Return a queryset containing all attachments for this model."""
|
||||||
return self.attachments_for_model().filter(model_id=self.pk)
|
return self.attachments_for_model().filter(model_id=self.pk)
|
||||||
|
|
||||||
@@ -1269,8 +1272,9 @@ class PathStringMixin(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@report.mixins.report_attribute()
|
||||||
def parentpath(self) -> list:
|
def parentpath(self) -> list:
|
||||||
"""Get the parent path of this category.
|
"""Construct the parent path of this tree node.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of category names from the top level to the parent of this category
|
List of category names from the top level to the parent of this category
|
||||||
@@ -1278,8 +1282,9 @@ class PathStringMixin(models.Model):
|
|||||||
return list(self.get_ancestors())
|
return list(self.get_ancestors())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@report.mixins.report_attribute()
|
||||||
def path(self) -> list:
|
def path(self) -> list:
|
||||||
"""Get the complete part of this category.
|
"""Construct the complete part of this tree node.
|
||||||
|
|
||||||
e.g. ["Top", "Second", "Third", "This"]
|
e.g. ["Top", "Second", "Third", "This"]
|
||||||
|
|
||||||
@@ -1468,6 +1473,7 @@ class InvenTreeBarcodeMixin(models.Model):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@report.mixins.report_attribute()
|
||||||
def barcode(self) -> str:
|
def barcode(self) -> str:
|
||||||
"""Format a minimal barcode string (e.g. for label printing)."""
|
"""Format a minimal barcode string (e.g. for label printing)."""
|
||||||
return self.format_barcode()
|
return self.format_barcode()
|
||||||
@@ -1646,14 +1652,26 @@ class InvenTreeImageMixin(models.Model):
|
|||||||
verbose_name=_('Image'),
|
verbose_name=_('Image'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_image_url(self):
|
def get_image_url(self) -> str:
|
||||||
"""Return the URL of the image for this object."""
|
"""Return the URL of the image for this object."""
|
||||||
if self.image:
|
if self.image:
|
||||||
return InvenTree.helpers.getMediaUrl(self.image)
|
return InvenTree.helpers.getMediaUrl(self.image)
|
||||||
return InvenTree.helpers.getBlankImage()
|
return InvenTree.helpers.getBlankImage()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@report.mixins.report_attribute()
|
||||||
|
def image_url(self) -> str:
|
||||||
|
"""Return the URL of the image for this object."""
|
||||||
|
return self.get_image_url()
|
||||||
|
|
||||||
def get_thumbnail_url(self) -> str:
|
def get_thumbnail_url(self) -> str:
|
||||||
"""Return the URL of the image thumbnail for this object."""
|
"""Return the URL of the image thumbnail for this object."""
|
||||||
if self.image:
|
if self.image:
|
||||||
return InvenTree.helpers.getMediaUrl(self.image, 'thumbnail')
|
return InvenTree.helpers.getMediaUrl(self.image, 'thumbnail')
|
||||||
return InvenTree.helpers.getBlankThumbnail()
|
return InvenTree.helpers.getBlankThumbnail()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@report.mixins.report_attribute()
|
||||||
|
def thumbnail_url(self) -> str:
|
||||||
|
"""Return the URL of the image thumbnail for this object."""
|
||||||
|
return self.get_thumbnail_url()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import TypedDict
|
from typing import Optional, TypedDict
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -229,7 +229,8 @@ class Company(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self):
|
@report.mixins.report_attribute()
|
||||||
|
def address(self) -> Optional[str]:
|
||||||
"""Return the string representation for the primary address.
|
"""Return the string representation for the primary address.
|
||||||
|
|
||||||
This property exists for backwards compatibility
|
This property exists for backwards compatibility
|
||||||
@@ -239,7 +240,8 @@ class Company(
|
|||||||
return str(addr) if addr is not None else None
|
return str(addr) if addr is not None else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def primary_address(self):
|
@report.mixins.report_attribute()
|
||||||
|
def primary_address(self) -> Optional['Address']:
|
||||||
"""Returns address object of primary address for this Company."""
|
"""Returns address object of primary address for this Company."""
|
||||||
# We may have a pre-fetched primary address list
|
# We may have a pre-fetched primary address list
|
||||||
if hasattr(self, 'primary_address_list'):
|
if hasattr(self, 'primary_address_list'):
|
||||||
@@ -254,7 +256,8 @@ class Company(
|
|||||||
return self.addresses.filter(primary=True).first()
|
return self.addresses.filter(primary=True).first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def currency_code(self):
|
@report.mixins.report_attribute()
|
||||||
|
def currency_code(self) -> str:
|
||||||
"""Return the currency code associated with this company.
|
"""Return the currency code associated with this company.
|
||||||
|
|
||||||
- If the currency code is invalid, use the default currency
|
- If the currency code is invalid, use the default currency
|
||||||
@@ -276,7 +279,8 @@ class Company(
|
|||||||
return InvenTree.helpers.pui_url(f'/purchasing/manufacturer/{self.id}')
|
return InvenTree.helpers.pui_url(f'/purchasing/manufacturer/{self.id}')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parts(self):
|
@report.mixins.report_attribute()
|
||||||
|
def parts(self) -> report.mixins.QuerySet['SupplierPart']:
|
||||||
"""Return SupplierPart objects which are supplied or manufactured by this company."""
|
"""Return SupplierPart objects which are supplied or manufactured by this company."""
|
||||||
return SupplierPart.objects.filter(
|
return SupplierPart.objects.filter(
|
||||||
Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id)
|
Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id)
|
||||||
|
|||||||
@@ -478,8 +478,9 @@ class Order(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_overdue(self):
|
@report.mixins.report_attribute()
|
||||||
"""Method to determine if this order is overdue.
|
def is_overdue(self) -> bool:
|
||||||
|
"""Determine if this order is overdue.
|
||||||
|
|
||||||
Makes use of the overdue_filter() method to avoid code duplication
|
Makes use of the overdue_filter() method to avoid code duplication
|
||||||
"""
|
"""
|
||||||
@@ -587,20 +588,23 @@ class Order(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def company(self):
|
@report.mixins.report_attribute()
|
||||||
"""Return the company associated with this order.
|
def company(self) -> Optional[Company]:
|
||||||
|
"""The company associated with this order.
|
||||||
|
|
||||||
This method must be implemented by any subclass, as the 'company' field may be named differently for different order types (e.g. supplier vs customer).
|
This method must be implemented by any subclass, as the 'company' field may be named differently for different order types (e.g. supplier vs customer).
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f'company() method not implemented for {__class__}')
|
raise NotImplementedError(f'company() method not implemented for {__class__}')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def order_address(self):
|
@report.mixins.report_attribute()
|
||||||
"""Return the Address associated with this order."""
|
def order_address(self) -> Optional[Address]:
|
||||||
|
"""The Address associated with this order."""
|
||||||
return self.address or self.company.primary_address
|
return self.address or self.company.primary_address
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_text(self):
|
@report.mixins.report_attribute()
|
||||||
|
def status_text(self) -> str:
|
||||||
"""Return the text representation of the current status. This will consider any custom status."""
|
"""Return the text representation of the current status. This will consider any custom status."""
|
||||||
if self.get_custom_status() is not None:
|
if self.get_custom_status() is not None:
|
||||||
from generic.states.custom import (
|
from generic.states.custom import (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||||
from typing import TypedDict, cast
|
from typing import Optional, TypedDict, cast
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@@ -35,6 +35,7 @@ from mptt.models import TreeForeignKey
|
|||||||
|
|
||||||
import common.currency
|
import common.currency
|
||||||
import common.models
|
import common.models
|
||||||
|
import company.models
|
||||||
import InvenTree.conversion
|
import InvenTree.conversion
|
||||||
import InvenTree.fields
|
import InvenTree.fields
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
@@ -1003,6 +1004,7 @@ class Part(
|
|||||||
return InvenTree.helpers.increment_serial_number(sn, self)
|
return InvenTree.helpers.increment_serial_number(sn, self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@report.mixins.report_attribute()
|
||||||
def full_name(self) -> str:
|
def full_name(self) -> str:
|
||||||
"""Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in InvenTree settings."""
|
"""Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in InvenTree settings."""
|
||||||
return part_helpers.render_part_full_name(self)
|
return part_helpers.render_part_full_name(self)
|
||||||
@@ -1214,7 +1216,8 @@ class Part(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_supplier(self):
|
@report.mixins.report_attribute()
|
||||||
|
def default_supplier(self) -> Optional[company.models.SupplierPart]:
|
||||||
"""Return the default (primary) SupplierPart for this Part.
|
"""Return the default (primary) SupplierPart for this Part.
|
||||||
|
|
||||||
This function is included for backwards compatibility,
|
This function is included for backwards compatibility,
|
||||||
@@ -1369,14 +1372,15 @@ class Part(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def category_path(self):
|
def category_path(self) -> str:
|
||||||
"""Return the category path of this Part instance."""
|
"""Return the category path of this Part instance."""
|
||||||
if self.category:
|
if self.category:
|
||||||
return self.category.pathstring
|
return self.category.pathstring
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available_stock(self):
|
@report.mixins.report_attribute()
|
||||||
|
def available_stock(self) -> Decimal:
|
||||||
"""Return the total available stock.
|
"""Return the total available stock.
|
||||||
|
|
||||||
- This subtracts stock which is already allocated to builds
|
- This subtracts stock which is already allocated to builds
|
||||||
@@ -1614,7 +1618,8 @@ class Part(
|
|||||||
PartStar.objects.filter(part=self, user=user).delete()
|
PartStar.objects.filter(part=self, user=user).delete()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_build(self):
|
@report.mixins.report_attribute()
|
||||||
|
def can_build(self) -> Decimal:
|
||||||
"""Return the number of units that can be build with available stock."""
|
"""Return the number of units that can be build with available stock."""
|
||||||
import part.filters
|
import part.filters
|
||||||
|
|
||||||
@@ -1646,7 +1651,7 @@ class Part(
|
|||||||
return int(max(can_build_quantity, 0))
|
return int(max(can_build_quantity, 0))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_builds(self):
|
def active_builds(self) -> int:
|
||||||
"""Return a list of outstanding builds.
|
"""Return a list of outstanding builds.
|
||||||
|
|
||||||
Builds marked as 'complete' or 'cancelled' are ignored
|
Builds marked as 'complete' or 'cancelled' are ignored
|
||||||
@@ -1654,12 +1659,10 @@ class Part(
|
|||||||
return self.builds.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
|
return self.builds.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_being_built(self, include_variants: bool = True):
|
@report.mixins.report_attribute()
|
||||||
|
def quantity_being_built(self) -> Decimal:
|
||||||
"""Return the current number of parts currently being built.
|
"""Return the current number of parts currently being built.
|
||||||
|
|
||||||
Arguments:
|
|
||||||
include_variants: If True, include variants of this part in the calculation
|
|
||||||
|
|
||||||
Note: This is the total quantity of Build orders, *not* the number of build outputs.
|
Note: This is the total quantity of Build orders, *not* the number of build outputs.
|
||||||
In this fashion, it is the "projected" quantity of builds
|
In this fashion, it is the "projected" quantity of builds
|
||||||
"""
|
"""
|
||||||
@@ -1667,12 +1670,8 @@ class Part(
|
|||||||
status__in=BuildStatusGroups.ACTIVE_CODES
|
status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
if include_variants:
|
# We are including variants, get all parts in the variant tree
|
||||||
# If we are including variants, get all parts in the variant tree
|
builds = builds.filter(part__in=self.get_descendants(include_self=True))
|
||||||
builds = builds.filter(part__in=self.get_descendants(include_self=True))
|
|
||||||
else:
|
|
||||||
# Only look at this part
|
|
||||||
builds = builds.filter(part=self)
|
|
||||||
|
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
@@ -1683,7 +1682,7 @@ class Part(
|
|||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_in_production(self, include_variants: bool = True):
|
def quantity_in_production(self, include_variants: bool = True) -> Decimal:
|
||||||
"""Quantity of this part currently actively in production.
|
"""Quantity of this part currently actively in production.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@@ -1877,7 +1876,8 @@ class Part(
|
|||||||
return query['t']
|
return query['t']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_stock(self):
|
@report.mixins.report_attribute()
|
||||||
|
def total_stock(self) -> Decimal:
|
||||||
"""Return the total stock quantity for this part.
|
"""Return the total stock quantity for this part.
|
||||||
|
|
||||||
- Part may be stored in multiple locations
|
- Part may be stored in multiple locations
|
||||||
@@ -2030,7 +2030,7 @@ class Part(
|
|||||||
return list(parts)
|
return list(parts)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_bom(self):
|
def has_bom(self) -> bool:
|
||||||
"""Return True if this Part instance has any BOM items."""
|
"""Return True if this Part instance has any BOM items."""
|
||||||
return self.get_bom_items().exists()
|
return self.get_bom_items().exists()
|
||||||
|
|
||||||
@@ -2042,7 +2042,7 @@ class Part(
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_trackable_parts(self):
|
def has_trackable_parts(self) -> bool:
|
||||||
"""Return True if any parts linked in the Bill of Materials are trackable.
|
"""Return True if any parts linked in the Bill of Materials are trackable.
|
||||||
|
|
||||||
This is important when building the part.
|
This is important when building the part.
|
||||||
@@ -2050,16 +2050,16 @@ class Part(
|
|||||||
return self.get_trackable_parts().exists()
|
return self.get_trackable_parts().exists()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bom_count(self):
|
def bom_count(self) -> int:
|
||||||
"""Return the number of items contained in the BOM for this part."""
|
"""Return the number of items contained in the BOM for this part."""
|
||||||
return self.get_bom_items().count()
|
return self.get_bom_items().count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def used_in_count(self):
|
def used_in_count(self) -> int:
|
||||||
"""Return the number of part BOMs that this part appears in."""
|
"""Return the number of part BOMs that this part appears in."""
|
||||||
return len(self.get_used_in())
|
return len(self.get_used_in())
|
||||||
|
|
||||||
def get_bom_hash(self):
|
def get_bom_hash(self) -> str:
|
||||||
"""Return a checksum hash for the BOM for this part.
|
"""Return a checksum hash for the BOM for this part.
|
||||||
|
|
||||||
Used to determine if the BOM has changed (and needs to be signed off!)
|
Used to determine if the BOM has changed (and needs to be signed off!)
|
||||||
@@ -2444,7 +2444,8 @@ class Part(
|
|||||||
return orders
|
return orders
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def on_order(self):
|
@report.mixins.report_attribute()
|
||||||
|
def on_order(self) -> Decimal:
|
||||||
"""Return the total number of items on order for this part.
|
"""Return the total number of items on order for this part.
|
||||||
|
|
||||||
Note that some supplier parts may have a different pack_quantity attribute,
|
Note that some supplier parts may have a different pack_quantity attribute,
|
||||||
|
|||||||
@@ -1,11 +1,45 @@
|
|||||||
"""Report mixin classes."""
|
"""Report mixin classes."""
|
||||||
|
|
||||||
from typing import Generic, TypedDict, TypeVar
|
from collections.abc import Callable
|
||||||
|
from typing import Generic, Optional, TypedDict, TypeVar
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
_Model = TypeVar('_Model', bound=models.Model, covariant=True)
|
_Model = TypeVar('_Model', bound=models.Model, covariant=True)
|
||||||
|
|
||||||
|
REPORT_ATTRIBUTE_MARKER = '_report_attribute_description'
|
||||||
|
|
||||||
|
|
||||||
|
def report_attribute(description: Optional[str] = None) -> Callable:
|
||||||
|
"""Mark a model property or method as discoverable for report documentation.
|
||||||
|
|
||||||
|
Unlike `InvenTreeReportMixin.report_context`, which only documents context data
|
||||||
|
explicitly assembled for a particular model, this decorator can be applied to any
|
||||||
|
property or method (including those defined on mixins shared by multiple models)
|
||||||
|
to make it show up in the generated report context documentation.
|
||||||
|
|
||||||
|
If no description is provided, the first line of the decorated function's
|
||||||
|
docstring is used instead.
|
||||||
|
|
||||||
|
When decorating a `@property`, this decorator must be applied to the underlying
|
||||||
|
function, i.e. *below* the `@property` decorator:
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
@report_attribute(description="Format a minimal barcode string")
|
||||||
|
def barcode(self) -> str:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
doc = (func.__doc__ or '').strip().splitlines()
|
||||||
|
setattr(func, REPORT_ATTRIBUTE_MARKER, description or (doc[0] if doc else ''))
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class QuerySet(Generic[_Model]):
|
class QuerySet(Generic[_Model]):
|
||||||
"""A custom QuerySet class used for type hinting in report context definitions.
|
"""A custom QuerySet class used for type hinting in report context definitions.
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ class StockLocation(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@report.mixins.report_attribute()
|
||||||
def icon(self) -> str:
|
def icon(self) -> str:
|
||||||
"""Get the current icon used for this location.
|
"""Get the current icon used for this location.
|
||||||
|
|
||||||
@@ -325,7 +326,8 @@ class StockLocation(
|
|||||||
return self.get_stock_items(cascade).count()
|
return self.get_stock_items(cascade).count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def item_count(self):
|
@report.mixins.report_attribute()
|
||||||
|
def item_count(self) -> int:
|
||||||
"""Simply returns the number of stock items in this location.
|
"""Simply returns the number of stock items in this location.
|
||||||
|
|
||||||
Required for tree view serializer.
|
Required for tree view serializer.
|
||||||
@@ -865,7 +867,8 @@ class StockItem(
|
|||||||
return self.get_next_serialized_item(reverse=True)
|
return self.get_next_serialized_item(reverse=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_label(self):
|
@report.mixins.report_attribute()
|
||||||
|
def status_label(self) -> str:
|
||||||
"""Return label."""
|
"""Return label."""
|
||||||
return StockStatus.label(self.status)
|
return StockStatus.label(self.status)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user