-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Refactored files - extended importpath to resolve InvenTree APIs - refactored templates into own files * Initial draft for bulk generator - added BulkGenerator - added parse API point - added bulk create API point - added basic template with preview and bulk create fetch calls * Added `AppMixin` and draft for models * Added __ini__.py to migrations * added model api point * Added initial setup * Added bulk creation panel to part categories * Added saved templates api * Removed settings.py * Added copy of htm and preact license * Added saved templates CRUD UI and global generateKeys management * Added option to load saved template * Added documentation (readme) and CI workflow * Fixed flake8 issues
- Loading branch information
Showing
26 changed files
with
1,804 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
name: CI | ||
|
||
on: | ||
push: | ||
pull_request: | ||
|
||
jobs: | ||
style: | ||
name: Style checks | ||
if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v2 | ||
|
||
- name: Setup python | ||
uses: actions/setup-python@v1 | ||
with: | ||
python-version: 3.10.9 | ||
|
||
- name: Install style check dependencies | ||
run: | | ||
pip install flake8==6.0.0 | ||
pip install pep8-naming==0.13.2 | ||
- name: Check style | ||
run: | | ||
flake8 . |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"python.analysis.extraPaths": ["/workspaces/InvenTree/InvenTree"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,97 @@ | ||
# inventree-bulk-plugin | ||
# inventree-bulk-plugin | ||
|
||
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) | ||
![CI](https://github.com/wolflu05/inventree-bulk-plugin/actions/workflows/ci.yml/badge.svg) | ||
|
||
> :warning: This plugin is currently in beta because it needs to be properly tested to be used in production. | ||
A bulk creation plugin for [InvenTree](https://inventree.org), which helps you generating locations/categories in bulk by using customized naming strategies and ensure them along your complete storage tree. | ||
|
||
## Installation | ||
|
||
1. Install this plugin as follows: | ||
|
||
```bash | ||
pip install git+https://github.com/wolflu05/inventree-bulk-plugin | ||
``` | ||
|
||
Or, add to your `plugins.txt` file: | ||
|
||
```txt | ||
git+https://github.com/wolflu05/inventree-bulk-plugin | ||
``` | ||
|
||
2. Goto your plugin settings and ensure that you allow the use of the url integration and app integration | ||
|
||
## Usage | ||
|
||
### Bulk create | ||
|
||
You can bulk create sub-stocklocations and sub-partcategories. Goto one and use the panel "Bulk-creation". Either load a [saved template](#saved-templates) or set up the output quickly. Use "Preview" to see how the bulk creation will look like and create to bulk create the locations/categories. To see how this editor works see [bulk creation editor](#bulk-creation-editor). | ||
|
||
### Saved templates | ||
|
||
You can save bulk creation templates to ensure consistency along your storage trees. Let's say you have a bunch of drawer towers. With saved templates you can now easily store your templates to re-use it when you want to add a new tower to the system. | ||
|
||
1. Goto the stock index and select the "Manage bulk creation" panel. | ||
2. Click on "New Template". | ||
3. Adjust the schema to your needs and use "Preview" to see how the creation will look like | ||
4. Create you template by using "Create" | ||
5. Goto the specific sub-location where you want to apply that template, load it and Bulk generate your locations to your needs. | ||
|
||
> :information_source: You can use inputs to make your bulk creation schema dynamic in amount of drawers or their names. | ||
### Bulk creation editor | ||
|
||
#### Input | ||
|
||
You can define key/value pairs of inputs which you can later reference in your schema via `{inp.<key>}`. This is useful for [saved templates](#saved-templates). | ||
|
||
#### Settings | ||
|
||
- `Count from` - defines from where to start with counting numbers in dimensions. | ||
- `Leading zeros` - defines if it needs to add leading zeros to numbers to ensure consistent length. | ||
|
||
#### Templates | ||
|
||
You can define templates from which you can later extend in your output. Template values can also be overwritten. | ||
|
||
- `Template name` - Template name, is later used to select for extending | ||
|
||
For the rest of the fields see [output](#output). | ||
|
||
#### Output | ||
|
||
##### Parent name match | ||
First child that matches the parent name matcher regex will be chosen for generating the child's for a specific parent. | ||
|
||
##### Extends | ||
Select a template to extend from | ||
|
||
##### Dimensions/Count | ||
Dimensions are a way to add various counting strategies to your naming. You can add a dimension by clicking on "Add dimension" and remove it via the red "X" on the right of the dimension field. | ||
|
||
A `dimension` can be either specify a range or a generic name. You can use the count field to limit a generic dimension to a specific amount of generating items. | ||
|
||
Ranges: `A-G`,`f-x`, `1-3`, `A-XZ` | ||
Generics: `NUMERIC` (0-9), `ALPHA_LOWER` (a-...), `ALPHA_UPPER` (A-...). | ||
|
||
##### Generate | ||
|
||
These fields my differ between stock location and part category. They correspond to the generated items property. For example "Generate Name" will be the name of the created location/category. | ||
|
||
> :information_source: You can use `{dim.<x>}` as a placeholder for the generated output of the dimension and `{inp.<key>}` for replacing with the given input value. Dimension numbering is starting with 1, so you can reference the value of the first dimension via `{dim.1}`. | ||
##### Child's | ||
|
||
Child's are a way to add some nesting to your bulk creation tree. You can use them for e.g. generating sections in every of your drawer. You can use the [Parent name match](#parent-name-match) option to add for your drawers named from `Drawer 1` - `Drawer 10` two sections while your other drawers have different sections. | ||
|
||
## FAQ | ||
|
||
#### Why does this plugin needs the App Mixin? | ||
|
||
> This plugin uses the App Mixin to add a custom model to the database to manage stored templates which ensure consistency along your creation of storage trees. (See [Saved templates](#saved-templates)) | ||
#### Why does this plugin needs the Url Mixin? | ||
|
||
> This plugin uses the Url Mixin to expose custom API endpoints for previewing and bulk create locations. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import json | ||
|
||
from django.conf.urls import url | ||
from django.http import JsonResponse, HttpResponse | ||
from django.views.decorators.csrf import csrf_exempt | ||
from django.forms.models import modelform_factory, model_to_dict | ||
from rest_framework import status | ||
|
||
from plugin import InvenTreePlugin | ||
from plugin.mixins import PanelMixin, UrlsMixin, AppMixin | ||
from stock.views import StockLocationDetail, StockIndex | ||
from stock.models import StockLocation | ||
from part.views import CategoryDetail | ||
from part.models import PartCategory | ||
|
||
from pydantic import ValidationError | ||
|
||
from .models import BulkCreationTemplate | ||
from .version import BULK_PLUGIN_VERSION | ||
from .BulkGenerator.BulkGenerator import BulkGenerator | ||
|
||
BulkCreationTemplateForm = modelform_factory( | ||
BulkCreationTemplate, fields=("name", "template_type", "template")) | ||
|
||
|
||
class BulkActionPlugin(AppMixin, PanelMixin, UrlsMixin, InvenTreePlugin): | ||
AUTHOR = "wolflu05" | ||
DESCRIPTION = "Bulk action plugin" | ||
VERSION = BULK_PLUGIN_VERSION | ||
|
||
TITLE = "Bulk Action" | ||
SLUG = "bulkaction" | ||
NAME = "Bulk Action" | ||
|
||
def get_custom_panels(self, view, request): | ||
panels = [] | ||
|
||
if isinstance(view, StockIndex): | ||
panels.append({ | ||
'title': 'Manage bulk creation', | ||
'icon': 'fas fa-tools', | ||
'content_template': 'panels/stock-index/manage-bulk.html', | ||
'javascript_template': 'panels/stock-index/manage-bulk.js', | ||
'description': 'Manage bulk creation', | ||
}) | ||
|
||
if isinstance(view, StockLocationDetail): | ||
panels.append({ | ||
'title': 'Bulk creation', | ||
'icon': 'fas fa-tools', | ||
'content_template': 'panels/stock-location-detail/create-bulk.html', | ||
'javascript_template': 'panels/stock-location-detail/create-bulk.js', | ||
'description': 'Bulk creation tools', | ||
}) | ||
|
||
if isinstance(view, CategoryDetail): | ||
panels.append({ | ||
'title': 'Bulk creation', | ||
'icon': 'fas fa-tools', | ||
'content_template': 'panels/category-detail/create-bulk.html', | ||
'javascript_template': 'panels/category-detail/create-bulk.js', | ||
'description': 'Bulk creation tools', | ||
}) | ||
|
||
return panels | ||
|
||
@csrf_exempt | ||
def url_parse(self, request): | ||
if request.method == "POST": | ||
error, output = self._parse_bulk_schema(request.body) | ||
if error is not None: | ||
return error | ||
|
||
return JsonResponse(output, safe=False) | ||
|
||
@csrf_exempt | ||
def url_bulk_create_location(self, request, pk): | ||
if request.method == "POST": | ||
error, output = self._parse_bulk_schema(request.body) | ||
if error is not None: | ||
return error | ||
|
||
root_location = StockLocation.objects.get(pk=pk) | ||
|
||
self._create_location(root_location, output) | ||
|
||
return HttpResponse(status=status.HTTP_201_CREATED) | ||
|
||
@csrf_exempt | ||
def url_bulk_create_category(self, request, pk): | ||
if request.method == "POST": | ||
error, output = self._parse_bulk_schema(request.body) | ||
if error is not None: | ||
return error | ||
|
||
root_category = PartCategory.objects.get(pk=pk) | ||
|
||
self._create_category(root_category, output) | ||
|
||
return HttpResponse(status=status.HTTP_201_CREATED) | ||
|
||
def _parse_bulk_schema(self, schema): | ||
try: | ||
parsed = json.loads(schema) | ||
bg = BulkGenerator(parsed).generate() | ||
return None, bg | ||
except (ValueError, ValidationError) as e: | ||
return JsonResponse({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST, safe=False), None | ||
except Exception: | ||
return JsonResponse({"error": "An error occured"}, status=status.HTTP_400_BAD_REQUEST, safe=False), None | ||
|
||
def _create_location(self, parent, childs): | ||
for c in childs: | ||
loc = StockLocation.objects.create( | ||
name=c[0]['name'], | ||
description=c[0]['description'], | ||
parent=parent | ||
) | ||
|
||
self._create_location(loc, c[1]) | ||
|
||
def _create_category(self, parent, childs): | ||
for c in childs: | ||
cat = PartCategory.objects.create( | ||
name=c[0]['name'], | ||
description=c[0]['description'], | ||
parent=parent | ||
) | ||
|
||
self._create_category(cat, c[1]) | ||
|
||
@csrf_exempt | ||
def url_templates(self, request, pk=None): | ||
if request.method == "POST": | ||
data = json.loads(request.body) | ||
populated_form = BulkCreationTemplateForm(data=data) | ||
if populated_form.is_valid(): | ||
saved = populated_form.save() | ||
return JsonResponse(model_to_dict(saved)) | ||
else: | ||
return JsonResponse(populated_form.errors.get_json_data(), status=status.HTTP_400_BAD_REQUEST) | ||
|
||
if request.method == "GET" and pk is None: | ||
templates = BulkCreationTemplate.objects.all() | ||
template_type = request.GET.get("template_type", None) | ||
if template_type is not None: | ||
templates = templates.filter(template_type=template_type) | ||
|
||
return JsonResponse(list(map(model_to_dict, templates)), safe=False) | ||
|
||
if pk is None: | ||
return HttpResponse(status=status.HTTP_404_NOT_FOUND) | ||
|
||
try: | ||
template = BulkCreationTemplate.objects.get(pk=pk) | ||
except BulkCreationTemplate.DoesNotExist: | ||
return HttpResponse(status=status.HTTP_404_NOT_FOUND) | ||
|
||
if request.method == "GET": | ||
return JsonResponse(model_to_dict(template)) | ||
|
||
if request.method == "PUT": | ||
body = json.loads(request.body) | ||
populated_form = BulkCreationTemplateForm( | ||
{**model_to_dict(template), **body, "id": template.pk}, instance=template) | ||
if populated_form.is_valid(): | ||
saved = populated_form.save() | ||
return JsonResponse(model_to_dict(saved)) | ||
else: | ||
return JsonResponse(populated_form.errors.get_json_data(), status=status.HTTP_400_BAD_REQUEST) | ||
|
||
if request.method == "DELETE": | ||
template.delete() | ||
return HttpResponse(status=status.HTTP_201_CREATED) | ||
|
||
return HttpResponse(status=status.HTTP_404_NOT_FOUND) | ||
|
||
def setup_urls(self): | ||
return [ | ||
url(r'parse', self.url_parse, name='parse'), | ||
url(r'bulkcreate/location/(?P<pk>\d+)', | ||
self.url_bulk_create_location, name='bulkcreatelocation'), | ||
url(r'bulkcreate/category/(?P<pk>\d+)', | ||
self.url_bulk_create_category, name='bulkcreatecategory'), | ||
url(r'templates/(?P<pk>\d+)', self.url_templates, name='templatebyid'), | ||
url(r'templates', self.url_templates, name='templates'), | ||
] |
Oops, something went wrong.