Skip to content

Commit

Permalink
v0.1.0 (#2)
Browse files Browse the repository at this point in the history
* 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
wolflu05 authored Dec 17, 2022
1 parent 4131827 commit 2c6cf24
Show file tree
Hide file tree
Showing 26 changed files with 1,804 additions and 44 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
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 .
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.analysis.extraPaths": ["/workspaces/InvenTree/InvenTree"]
}
98 changes: 97 additions & 1 deletion README.md
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.
187 changes: 187 additions & 0 deletions inventree_bulk/BulkActionPlugin.py
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'),
]
Loading

0 comments on commit 2c6cf24

Please sign in to comment.