Skip to content

Commit

Permalink
Move files and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
LavissaWoW committed Mar 19, 2024
1 parent 96dac16 commit fce94a2
Show file tree
Hide file tree
Showing 7 changed files with 643 additions and 52 deletions.
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: CI

on:
push:
pull_request:

jobs:
style:
name: "Style Checks"
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.10"

- name: Install dependencies
run: |
pip install flake8
pip install pep8-naming
- name: Perform Style Check
run: |
flake8 .
test-plugin:
name: "Unit Tests"
runs-on: ubuntu-latest

container:
image: inventree/inventree:latest
env:
INVENTREE_DB_ENGINE: postgresql
INVENTREE_DB_NAME: inventree
INVENTREE_DB_HOST: db
INVENTREE_DB_PORT: 5432
INVENTREE_DB_USER: inventree
INVENTREE_DB_PASSWORD: inventree
INVENTREE_PLUGINS_ENABLED: True
INVENTREE_PLUGIN_TESTING: True
PLUGIN_TESTING_EVENTS: True
INVENTREE_PLUGIN_TESTING_SETUP: True

services:
db:
image: postgres:13
ports:
- 5432:5432
env:
POSTGRES_USER: inventree
POSTGRES_PASSWORD: inventree
POSTGRES_DB: inventree

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup InvenTree
run: |
cd /home/inventree
pip3 install --no-cache-dir --disable-pip-version-check -U -r requirements.txt
- name: Setup IPN Generator Plugin
run: |
cd /home/inventree
pip3 install -e $GITHUB_WORKSPACE
- name: Run Tests
run: |
cd /home/inventree
invoke test
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,36 @@
This is a plugin for [InvenTree](https://github.com/inventree/InvenTree/).
Installing this plugin enables the automatic generation if Internal Part Numbers (IPN) for parts.

## Installation
NOT YET PUBLISHED!

To automatically install the plugin when running `invoke install`:
Add `inventree-ipn-generator` to your plugins.txt file.

Or, install the plugin manually:

```
pip install inventree-ipn-generator
```

## Settings

- Active - Enables toggling of plugin without having to disable it
- On Create - If on, the plugin will assign IPNs to newly created parts
- On Change - If on, the plugin will assign IPNs to parts after a change has been made.
Enabling this setting will remove the ability to have parts without IPNs.

## Pattern
Part Number patterns follow three basic groups. Literals, Numerics, and characters.
When incrementing a part number, the rightmost group that is mutable will be incremented.
All groups can be combined in any order.

A pattern cannot consist of _only_ Literals.

### Literals (Characters that won't change)
Anything encased in `()` will be rendered as is. no change will be made to anything within.
### Literals (Immutable)
Anything encased in `()` will be rendered as-is. no change will be made to anything within.

Example: `(A5C)` will _always_ render as "A5C", regardless of other groups
Example: `(A6C)` will _always_ render as "A6C", regardless of other groups

### Numeric
Numbers that should change over time should be encased in `{}`
Expand All @@ -31,5 +53,5 @@ These two directives can be combined.

### Examples
1. `(AB){3}[ab]` -> AB001a, AB001b, AB002a, AB021b, AB032a, etc
2. `{2}[Aq](BD)` -> 01ABD, 01bBD, 02ABD, 02bBD, etc
2. `{2}[Aq](BD)` -> 01ABD, 01qBD, 02ABD, 02qBD, etc
3. `{1}[a-d]{8+}` -> 1a8, 1a9, 1b8, 1b9, 1c8, 1c9, 1d8, 1d9, 2a8, etc
File renamed without changes.
128 changes: 83 additions & 45 deletions src/generator.py → ipn_generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
from plugin.mixins import EventMixin, SettingsMixin
from part.models import Part

from django.core.exceptions import ValidationError

import logging
import re

# translation
from django.utils.translation import ugettext_lazy as _

logger = logging.getLogger('inventree')

def validate_pattern(pattern):
"""Validates pattern groups"""
regex = re.compile(r"(\{\d+\+?\})|(\[(?!\w\])(?:\w+|(?:\w-\w)+)+\])")
if not regex.search(pattern):
raise ValidationError("Pattern must include more than Literals")

return True

class AutoGenIPNPlugin(EventMixin, SettingsMixin, InvenTreePlugin):
"""Plugin to generate IPN automatically"""

Expand All @@ -25,27 +32,28 @@ class AutoGenIPNPlugin(EventMixin, SettingsMixin, InvenTreePlugin):

SETTINGS = {
'ACTIVE': {
'name': _('Active'),
'description': _('IPN generator is active'),
'name': ('Active'),
'description': ('IPN generator is active'),
'validator': bool,
'default': True
},
'ON_CREATE': {
'name': _('On Create'),
'description': _('Active when creating new parts'),
'name': ('On Create'),
'description': ('Active when creating new parts'),
'validator': bool,
'default': True
},
'ON_CHANGE': {
'name': _('On Edit'),
'description': _('Active when editing existing parts'),
'name': ('On Edit'),
'description': ('Active when editing existing parts'),
'validator': bool,
'default': True
},
'PATTERN': {
'name': _('IPN pattern'),
'description': _('Pattern for IPN generation'),
'default': ''
'name': ('IPN pattern'),
'description': ('Pattern for IPN generation'),
'default': "(12)[a-z][a-d]",
'validator': validate_pattern
},
}

Expand All @@ -54,42 +62,59 @@ class AutoGenIPNPlugin(EventMixin, SettingsMixin, InvenTreePlugin):
skip_chars = range(ord('['), ord('a'))

def wants_process_event(self, event):
"""Lets InvenTree know what events to listen for."""

if not self.get_setting('ACTIVE'):
return False

return event in ['part_part.saved', 'part_part.created']
if (event == 'part_part.saved'):
return self.get_setting('ON_CHANGE')

if (event == 'part_part.created'):
return self.get_setting('ON_CREATE')

return False

def process_event(self, event, *args, **kwargs):
"""Main plugin handler function"""

if not self.get_setting('ACTIVE'):
return False

id = kwargs.pop('id', None)
model = kwargs.pop('model', None)

# Events can fire on unrelated models
if model != "Part":
logger.debug('IPN Generator: Event Model is not part')
return

# Don't create IPNs for parts with IPNs
part = Part.objects.get(id=id)
if part.IPN:
return

expression = self.construct_regex()

expression = self.construct_regex(True)
latest = Part.objects.filter(IPN__regex=expression).order_by('-IPN').first()

if not latest:
part.IPN = self.construct_first_ipn()
else:
part.IPN = self.increment_ipn(expression, latest.IPN)

grouped_expression = self.construct_regex()
part.IPN = self.increment_ipn(grouped_expression, latest.IPN)

part.save()

return


def construct_regex(self):

def construct_regex(self, disable_groups=False):
"""Constructs a valid regex from provided IPN pattern.
This regex is used to find the latest assigned IPN
"""
regex = '^'

m = re.findall(r"(\{\d+\+?\})|(\([^\d\(\)]+\))|(\[(?:\w+|\w-\w)+\])", self.get_setting('PATTERN'))
m = re.findall(r"(\{\d+\+?\})|(\([\w\(\)]+\))|(\[(?:\w+|\w-\w)+\])", self.get_setting('PATTERN'))

for idx, group in enumerate(m):
numeric, literal, character = group
Expand All @@ -98,24 +123,33 @@ def construct_regex(self):
start = "+" in numeric
r = ''
g = numeric.strip("{}+")
if start:
regex += f'(?P<Ni{idx}>'
if start:
regex += '('
if not disable_groups:
regex += f'?P<Np{g}i{idx}>'
for char in g:
regex += f'[{char}-9]'
else:
regex += f'(?P<N{g}i{idx}>'
regex += '('
if not disable_groups:
regex += f'?P<N{g}i{idx}>'
regex += f'\d{ {int(g)} }'
regex += ')'

# Literal, won't change
if literal:
l = literal.strip("()")
regex += f'(?P<Li{idx}>{re.escape(l)})'
regex += '('
if not disable_groups:
regex += f'?P<Li{idx}>'
regex += f'{re.escape(l)})'

# Letters, a collection or sequence
# Sequences incremented using ASCII
if character:
regex += f'(?P<C'
regex += '('
if not disable_groups:
regex += f'?P<C'

sequences = re.findall(r'(\w)(?!-)|(\w\-\w)', character)

Expand All @@ -127,9 +161,10 @@ def construct_regex(self):
exp.append(single)
elif range:
exp.append(range)


regex += f'{"_".join(exp).replace("-", "")}i{idx}>'


if not disable_groups:
regex += f'{"_".join(exp).replace("-", "")}i{idx}>'
regex += f'[{"".join(exp)}]'
regex += ')'

Expand All @@ -138,7 +173,7 @@ def construct_regex(self):
return regex

def increment_ipn(self, exp, latest):

"""Deconstructs IPN pattern based on latest IPN and constructs a the next IPN in the series."""
m: re.Match = re.match(exp, latest)

ipn_list = []
Expand Down Expand Up @@ -167,7 +202,7 @@ def increment_ipn(self, exp, latest):
if not ranges:
if choices.index(val) == len(choices) - 1:
ipn_list.append(choices[0])
else:
else:
ipn_list.append(choices[choices.index(val) + 1])
incremented = True
else:
Expand All @@ -176,7 +211,7 @@ def increment_ipn(self, exp, latest):
min = ord(choice[0])
max = ord(choice[1])
if integerized_char in range(min, max + 1):
if integerized_char == max -1:
if integerized_char == max:
ipn_list.append(choice[0])
else:
ipn_list.append(chr(integerized_char + 1))
Expand All @@ -188,8 +223,18 @@ def increment_ipn(self, exp, latest):
break

elif type.startswith('N'):
num = int(type[1:])
if len(str(int(val) + 1)) > num:
if type[1] == 'p':
num = int(type[2:])
else:
num = int(type[1:])
if type[1] == 'p':
starts = int(type[2:])
next = int(val) + 1
if len(str(next)) > len(type[2:]):
ipn_list.append(type[2:])
else:
ipn_list.append(str(next))
elif len(str(int(val) + 1)) > num:
ipn_list.append(str(1).zfill(num))
else:
ipn_list.append(str(int(val)+1).zfill(num))
Expand All @@ -201,17 +246,16 @@ def increment_ipn(self, exp, latest):


def construct_first_ipn(self):

m = re.findall(r"(\{\d+\+?\})|(\([^\d\(\)]+\))|(\[(?:\w+|(?:\w-\w)+)\])", self.get_setting('PATTERN'))
"""No IPNs matching the pattern were found. Constructing the first IPN."""
m = re.findall(r"(\{\d+\+?\})|(\([\w\(\)]+\))|(\[(?:\w+|(?:\w-\w)+)\])", self.get_setting('PATTERN'))

ipn = ''

for group in m:
numeric, literal, character = group

if numeric:
num = numeric.strip("{}+")
if "+" in numeric:
if "+" in numeric:
ipn += num
else:
ipn += str(1).zfill(int(num))
Expand All @@ -220,12 +264,6 @@ def construct_first_ipn(self):
ipn += literal.strip("()")

if character:
ipn += character[0]
ipn += character.strip("[]")[0]

return ipn






Empty file added ipn_generator/tests/__init__.py
Empty file.
Loading

0 comments on commit fce94a2

Please sign in to comment.