diff --git a/README.md b/README.md index 674be30..0d4c800 100644 --- a/README.md +++ b/README.md @@ -6,30 +6,135 @@ The **inventree-wireviz** plugin provides direct integration for [wireviz](https://github.com/formatc1702/WireViz), a text-based wiring harness specification tool. -TODO: Description +## Functionality + +The plugin provides a number of key functions: + +### Harness Diagram Generation + +This plugin provides server-side generation of a wiring harness diagram from a `.wireviz` file. Uploading a simple [harness file](./demo/harness.wireviz) results in the generation of a wiring diagram: + +![](./demo/harness.svg) + +> **Note** +> Refer to the [wireviz syntax guide](https://github.com/formatc1702/WireViz/blob/master/docs/syntax.md) for a full description of the file format. + +The generated harness diagram is available as a `.svg` file. + +### BOM Extraction + +Bill of Materials (BOM) information can be extracted directly from the harness description file, allowing for a harness assembly to be fully qualified from the template file. + +### Report Generation + +The generated `.svg` can be used in report templates, for example as a reference diagram in a Build Order Report ## Installation -### System Requirements +### Installation Requirements You must have [graphviz](https://graphviz.org/) installed, and accessible by the InvenTree server. e.g. `apt-get install graphviz` -## Configuration +If installing in a container environment (e.g. Docker), the dockerfile will need to be extended to install the *graphviz* binaries + +### Plugin Installation + +The plugin is available [via PIP](https://pypi.org/project/inventree-wireviz-plugin/). Follow the [InvenTree plugin installation guide](https://docs.inventree.org/en/latest/extend/plugins/install/) to install the plugin on your system + +### Configuration + +Once the plugin is installed, it needs to be enabled before it is available for use. Again, refer to the InvenTree docs for instructions on how to enable the plugin. After the plugin is enabled, the following configuration options are available: + +![](./docs/config.png) + +| Setting | Description | +| --- | --- | +| Wireviz Upload Path | Directory where wireviz *template* files can be uploaded, and referenced by wireviz. This is an *advanced* option. Refer to the wireviz docs for more information on templates. | +| Delete Old Files | Remove old harness diagram files when a new `.wireviz` file is uploaded | +| Extract BOM Data | Extract BOM data from harness file and generate new BOM entries | +| Clear BOM Data | Remove existing BOM entries first, before creating new ones | +| Add Part Image | Where available, embed part images in the generated harness diagram | + +Additionally, you must ensure that the *Enable event integration* setting is enabled in the *Plugin Settings* view: + +![](./docs/event_plugin.png) + +## Operation + +### Uploading Wireviz File + +To generate a wiring harness diagram for a specific Part, upload a (valid) wireviz yaml file. The file **must* have the `.wireviz` extension to be recognized by the plugin. + +When the file is uploaded to the server, the plugin is notified and begins the process of generating the harness diagram. If successful, a `.svg` file is attached to the part instance: + +![](./docs/svg_file.png) + +### Harness Diagram Panel + +When a Part has a valid harness diagram (i.e. generated without any critical errors), the *Harness Diagram* panel will be available for that part. This panel displays the diagram image, and a simple Bill of Materials (as defined in the uploaded `.wireviz` file): + +![](./docs/harness_panel.png) + +> **Note** +> You may need to reload the page before this panel is visible + +> **Warning** +> Any warnings or errors which were raised during the process will be displayed here + +### BOM Extraction + +If enabled, the plugin will attempt to generate a linked Bill of Materials based on the data provided in the file. Part linking is performed based on the `pn` (part number) attribute in the wireviz BOM. + +For each line item in the uploaded BOM, the plugin attempts to match the `pn` field to an existing part in the InvenTree database. If a matching part is not found, this is marked with a warning in the simplified BOM table in the [harness diagram panel](#harness-diagram-panel). + +### Reports + +The generated diagram can be used in certain reports (such as the Build Order Report). If a wiring harness diagram is available for a Part, it is included in the report context as a variable named `wireviz_svg_file`. + +> **Note** +> The provided variable refers to the *filename* of the `.svg` image - not the file itself. +> Use the `{% encode_svg_image %}` template tag to render the image file. + +A very simple example is shown below: + +```html +{% extends "report/inventree_build_order_base.html" %} + +{% load report %} + +{% block style %} +{{ block.super }} + +.harness { + border: 1px solid #AAA; + width: 100%; + display: inline-block; +} + +{% endblock %} + +{% block page_content %} + +{{ block.super }} + +{% if wireviz_svg_file %} + +

Harness Drawing

+ +{% endif %} -**TODO** -## Documentation +{% endblock page_content %} +``` -### Template Files +The resulting report is rendered as below: -**TODO** -### Part Images +![](./docs/report.png) -**TODO** -### Wireviz Documentation +## Wireviz Documentation -Documentation on the capabilites of wireviz itself: +Documentation on the capabilities of wireviz itself: - https://github.com/formatc1702/WireViz/blob/master/docs/README.md - https://github.com/formatc1702/WireViz/blob/master/docs/syntax.md diff --git a/demo/harness.svg b/demo/harness.svg new file mode 100644 index 0000000..031b088 --- /dev/null +++ b/demo/harness.svg @@ -0,0 +1,176 @@ + + + + + + +%3 + + + +X1 + + +X1 + +P/N: DSUB-9 + +D-Sub + +female + +9-pin + +DCD + +1 + +RX + +2 + +TX + +3 + +DTR + +4 + +GND + +5 + +DSR + +6 + +RTS + +7 + +CTS + +8 + +RI + +9 + + + +W1 + + +W1 + +P/N: WIRE-SHIELDED + +3x + +0.25 mm² + ++ S + +0.2 m +  +X1:5:GND +     1:WH     +X2:1:GND + + + +X1:2:RX +     2:BN     +X2:3:TX + + + +X1:3:TX +     3:GN     +X2:2:RX + + + +  +X1:5:GND +Shield + +  + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + +X2 + + +X2 + +P/N: KK-254 + +Molex KK 254 + +female + +3-pin + +1 + +GND + +2 + +RX + +3 + +TX + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + diff --git a/demo/harness.wireviz b/demo/harness.wireviz new file mode 100644 index 0000000..47bb636 --- /dev/null +++ b/demo/harness.wireviz @@ -0,0 +1,29 @@ +connectors: + X1: + type: D-Sub + subtype: female + pn: DSUB-9 + pinlabels: [DCD, RX, TX, DTR, GND, DSR, RTS, CTS, RI] + X2: + type: Molex KK 254 + subtype: female + pn: KK-254 + pinlabels: [GND, RX, TX] + +cables: + W1: + gauge: 0.25 mm2 + length: 0.2 m + color_code: DIN + wirecount: 3 + shield: true + pn: WIRE-SHIELDED + +connections: + - + - X1: [5,2,3] + - W1: [1,2,3] + - X2: [1,3,2] + - + - X1: 5 + - W1: s \ No newline at end of file diff --git a/docs/config.png b/docs/config.png new file mode 100644 index 0000000..dd88710 Binary files /dev/null and b/docs/config.png differ diff --git a/docs/event_plugin.png b/docs/event_plugin.png new file mode 100644 index 0000000..d54be5d Binary files /dev/null and b/docs/event_plugin.png differ diff --git a/docs/harness_panel.png b/docs/harness_panel.png new file mode 100644 index 0000000..8f374d8 Binary files /dev/null and b/docs/harness_panel.png differ diff --git a/docs/report.png b/docs/report.png new file mode 100644 index 0000000..3abb392 Binary files /dev/null and b/docs/report.png differ diff --git a/docs/svg_file.png b/docs/svg_file.png new file mode 100644 index 0000000..d80e47a Binary files /dev/null and b/docs/svg_file.png differ diff --git a/inventree_wireviz/templates/wireviz/harness_panel.html b/inventree_wireviz/templates/wireviz/harness_panel.html index 94c4582..ef5975b 100644 --- a/inventree_wireviz/templates/wireviz/harness_panel.html +++ b/inventree_wireviz/templates/wireviz/harness_panel.html @@ -71,5 +71,5 @@ {% if wireviz_source_file %} -Harness source file: {{ wireviz_source_file }} +Source file {% endif %} diff --git a/inventree_wireviz/wireviz.py b/inventree_wireviz/wireviz.py index 1708c3f..36f19f2 100644 --- a/inventree_wireviz/wireviz.py +++ b/inventree_wireviz/wireviz.py @@ -17,8 +17,9 @@ from django.db import transaction from plugin import InvenTreePlugin -from plugin.mixins import EventMixin, PanelMixin, SettingsMixin +from plugin.mixins import EventMixin, PanelMixin, ReportMixin, SettingsMixin +from build.views import BuildDetail from company.models import ManufacturerPart, SupplierPart from InvenTree.api_version import INVENTREE_API_VERSION from part.models import BomItem, Part, PartAttachment @@ -30,7 +31,7 @@ logger = logging.getLogger('inventree') -class WirevizPlugin(EventMixin, PanelMixin, SettingsMixin, InvenTreePlugin): +class WirevizPlugin(EventMixin, PanelMixin, ReportMixin, SettingsMixin, InvenTreePlugin): """"Wireviz plugin for InvenTree - Provides a custom panel for rendering wireviz diagrams @@ -87,34 +88,69 @@ class WirevizPlugin(EventMixin, PanelMixin, SettingsMixin, InvenTreePlugin): }, } + def get_part_from_instance(self, instance): + """Return a Part object from the given instance.""" + + if not instance: + return None + + if isinstance(instance, Part): + return instance + + if hasattr(instance, 'part') and isinstance(instance.part, Part): + return instance.part + + # No match + return None + + def add_report_context(self, report_instance, model_instance, request, context): + """Inject wireviz data into the report context.""" + + # Extract a Part model from the model instance + part = self.get_part_from_instance(model_instance) + + if isinstance(part, Part): + metadata = part.get_metadata('wireviz') + + if metadata: + if svg_file := metadata.get(self.HARNESS_SVG_KEY, None): + context['wireviz_svg_file'] = svg_file + + if bom_data := metadata.get(self.HARNESS_BOM_KEY, None): + context['wireviz_bom_data'] = bom_data + def get_panel_context(self, view, request, context): """Return context information for the Wireviz panel.""" try: - part = view.get_object() + instance = view.get_object() except AttributeError: return context - - if isinstance(view, PartDetail) and isinstance(part, Part): + + part = self.get_part_from_instance(instance) + + if part and isinstance(part, Part): # Get wireviz file information from part metadata wireviz_metadata = part.get_metadata('wireviz') - svg_file = wireviz_metadata.get(self.HARNESS_SVG_KEY, None) - bom_data = wireviz_metadata.get(self.HARNESS_BOM_KEY, None) - src_file = wireviz_metadata.get(self.HARNESS_SRC_KEY, None) - if svg_file: - context['wireviz_svg_file'] = os.path.join(settings.MEDIA_URL, svg_file) + if wireviz_metadata: + svg_file = wireviz_metadata.get(self.HARNESS_SVG_KEY, None) + bom_data = wireviz_metadata.get(self.HARNESS_BOM_KEY, None) + src_file = wireviz_metadata.get(self.HARNESS_SRC_KEY, None) - if src_file: - context['wireviz_source_file'] = os.path.join(settings.MEDIA_URL, src_file) + if svg_file: + context['wireviz_svg_file'] = os.path.join(settings.MEDIA_URL, svg_file) - if bom_data: - context['wireviz_bom_data'] = bom_data - - # Add warnings and errors - context['wireviz_warnings'] = wireviz_metadata.get('warnings', None) - context['wireviz_errors'] = wireviz_metadata.get('errors', None) + if src_file: + context['wireviz_source_file'] = os.path.join(settings.MEDIA_URL, src_file) + + if bom_data: + context['wireviz_bom_data'] = bom_data + + # Add warnings and errors + context['wireviz_warnings'] = wireviz_metadata.get('warnings', None) + context['wireviz_errors'] = wireviz_metadata.get('errors', None) return context @@ -127,21 +163,26 @@ def get_custom_panels(self, view, request): instance = view.get_object() except AttributeError: return panels + + part = self.get_part_from_instance(instance) + + # A valid part object has been found + if part and isinstance(part, Part): - if isinstance(view, PartDetail): - part = instance + # We are on the PartDetail or BuildDetail page + if isinstance(view, PartDetail) or isinstance(view, BuildDetail): - logger.debug(f"Checking for wireviz file for part {part}") + logger.debug(f"Checking for wireviz file for part {part}") - metadata = part.get_metadata('wireviz') + metadata = part.get_metadata('wireviz') - if metadata: - panels.append({ - 'title': 'Harness Diagram', - 'icon': 'fas fa-project-diagram', - 'content_template': 'wireviz/harness_panel.html', - 'javascript_template': 'wireviz/harness_panel.js', - }) + if metadata: + panels.append({ + 'title': 'Harness Diagram', + 'icon': 'fas fa-project-diagram', + 'content_template': 'wireviz/harness_panel.html', + 'javascript_template': 'wireviz/harness_panel.js', + }) return panels @@ -185,6 +226,10 @@ def cleanup_old_files(self, filename: str, part: Part): metadata = part.get_metadata('wireviz') + if not metadata: + # No metadata to check + return + filenames = [] for key in file_keys: @@ -361,13 +406,18 @@ def extract_bom_data(self, harness: Harness): }) if not sub_part: - self.add_warning(f"No matching part for line: {description}") + # No matching part can be found continue if sub_part == self.part: self.add_error(f"Part {sub_part} is the same as the parent part") continue + # Check that it is a *valid* option for the BOM + if not sub_part.check_add_to_bom(self.part): + self.add_error(f"Part {sub_part} is not a valid option for the BOM") + continue + # Associate the internal part with the designators for designator in designators: self.part_map[designator] = sub_part