2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 04:26:44 +00:00

Merge branch 'master' of git://github.com/inventree/InvenTree into bom_export_parameter_stock

This commit is contained in:
eeintech 2020-08-25 15:59:58 -05:00
commit 109307858a
24 changed files with 472 additions and 334 deletions

View File

@ -12,16 +12,17 @@ addons:
before_install: before_install:
- sudo apt-get update - sudo apt-get update
- sudo apt-get install gettext - sudo apt-get install gettext
- make install - pip3 install invoke
- make migrate - invoke install
- invoke migrate
- cd InvenTree && python3 manage.py createsuperuser --username InvenTreeAdmin --email admin@inventree.com --noinput && cd .. - cd InvenTree && python3 manage.py createsuperuser --username InvenTreeAdmin --email admin@inventree.com --noinput && cd ..
script: script:
- cd InvenTree && python3 manage.py makemigrations && cd .. - cd InvenTree && python3 manage.py makemigrations && cd ..
- python3 ci/check_migration_files.py - python3 ci/check_migration_files.py
- make coverage - invoke coverage
- make translate - invoke translate
- make style - invoke style
after_success: after_success:
- coveralls - coveralls

View File

@ -163,7 +163,7 @@ LOGGING = {
}, },
} }
MIDDLEWARE = [ MIDDLEWARE = CONFIG.get('middleware', [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
@ -173,9 +173,12 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'InvenTree.middleware.AuthRequiredMiddleware' 'InvenTree.middleware.AuthRequiredMiddleware'
] ])
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
'django.contrib.auth.backends.ModelBackend'
])
# If the debug toolbar is enabled, add the modules # If the debug toolbar is enabled, add the modules
if DEBUG and CONFIG.get('debug_toolbar', False): if DEBUG and CONFIG.get('debug_toolbar', False):

View File

@ -87,3 +87,20 @@ latex:
interpreter: pdflatex interpreter: pdflatex
# Extra options to pass through to the LaTeX interpreter # Extra options to pass through to the LaTeX interpreter
options: '' options: ''
# Permit custom authentication backends
#authentication_backends:
# - 'django.contrib.auth.backends.ModelBackend'
# Custom middleware, sometimes needed alongside an authentication backend change.
#middleware:
# - 'django.middleware.security.SecurityMiddleware'
# - 'django.contrib.sessions.middleware.SessionMiddleware'
# - 'django.middleware.locale.LocaleMiddleware'
# - 'django.middleware.common.CommonMiddleware'
# - 'django.middleware.csrf.CsrfViewMiddleware'
# - 'corsheaders.middleware.CorsMiddleware'
# - 'django.contrib.auth.middleware.AuthenticationMiddleware'
# - 'django.contrib.messages.middleware.MessageMiddleware'
# - 'django.middleware.clickjacking.XFrameOptionsMiddleware'
# - 'InvenTree.middleware.AuthRequiredMiddleware'

View File

@ -8,7 +8,7 @@ from .models import StockItemLabel
class StockItemLabelAdmin(admin.ModelAdmin): class StockItemLabelAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'label') list_display = ('name', 'description', 'label', 'filters', 'enabled')
admin.site.register(StockItemLabel, StockItemLabelAdmin) admin.site.register(StockItemLabel, StockItemLabelAdmin)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-08-22 23:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('label', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='stockitemlabel',
name='enabled',
field=models.BooleanField(default=True, help_text='Label template is enabled', verbose_name='Enabled'),
),
]

View File

@ -70,6 +70,12 @@ class LabelTemplate(models.Model):
validators=[validateFilterString] validators=[validateFilterString]
) )
enabled = models.BooleanField(
default=True,
help_text=_('Label template is enabled'),
verbose_name=_('Enabled')
)
def get_record_data(self, items): def get_record_data(self, items):
""" """
Return a list of dict objects, one for each item. Return a list of dict objects, one for each item.

View File

@ -51,7 +51,8 @@ class PartResource(ModelResource):
report_skipped = False report_skipped = False
clean_model_instances = True clean_model_instances = True
exclude = [ exclude = [
'bom_checksum', 'bom_checked_by', 'bom_checked_date' 'bom_checksum', 'bom_checked_by', 'bom_checked_date',
'lft', 'rght', 'tree_id', 'level',
] ]
def get_queryset(self): def get_queryset(self):

View File

@ -41,7 +41,6 @@ from InvenTree.helpers import decimal2string, normalize
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
from report import models as ReportModels
from build import models as BuildModels from build import models as BuildModels
from order import models as OrderModels from order import models as OrderModels
from company.models import SupplierPart from company.models import SupplierPart
@ -399,24 +398,6 @@ class Part(MPTTModel):
self.category = category self.category = category
self.save() self.save()
def get_test_report_templates(self):
"""
Return all the TestReport template objects which map to this Part.
"""
templates = []
for report in ReportModels.TestReport.objects.all():
if report.matches_part(self):
templates.append(report)
return templates
def has_test_report_templates(self):
""" Return True if this part has a TestReport defined """
return len(self.get_test_report_templates()) > 0
def get_absolute_url(self): def get_absolute_url(self):
""" Return the web URL for viewing this part """ """ Return the web URL for viewing this part """
return reverse('part-detail', kwargs={'pk': self.id}) return reverse('part-detail', kwargs={'pk': self.id})

View File

@ -3,13 +3,12 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import ReportTemplate, ReportAsset from .models import TestReport, ReportAsset
from .models import TestReport
class ReportTemplateAdmin(admin.ModelAdmin): class ReportTemplateAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'template') list_display = ('name', 'description', 'template', 'filters', 'enabled')
class ReportAssetAdmin(admin.ModelAdmin): class ReportAssetAdmin(admin.ModelAdmin):
@ -17,6 +16,5 @@ class ReportAssetAdmin(admin.ModelAdmin):
list_display = ('asset', 'description') list_display = ('asset', 'description')
admin.site.register(ReportTemplate, ReportTemplateAdmin)
admin.site.register(TestReport, ReportTemplateAdmin) admin.site.register(TestReport, ReportTemplateAdmin)
admin.site.register(ReportAsset, ReportAssetAdmin) admin.site.register(ReportAsset, ReportAssetAdmin)

View File

@ -0,0 +1,16 @@
# Generated by Django 3.0.7 on 2020-08-22 23:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('report', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='ReportTemplate',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-08-23 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('report', '0002_delete_reporttemplate'),
]
operations = [
migrations.AddField(
model_name='testreport',
name='enabled',
field=models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-08-23 11:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('report', '0003_testreport_enabled'),
]
operations = [
migrations.RenameField(
model_name='testreport',
old_name='part_filters',
new_name='filters',
),
]

View File

@ -16,9 +16,11 @@ from django.conf import settings
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from stock.models import StockItem
from part import models as PartModels from InvenTree.helpers import validateFilterString
from django.utils.translation import gettext_lazy as _
try: try:
from django_weasyprint import WeasyTemplateResponseMixin from django_weasyprint import WeasyTemplateResponseMixin
@ -55,59 +57,6 @@ def rename_template(instance, filename):
return os.path.join('report', 'report_template', instance.getSubdir(), filename) return os.path.join('report', 'report_template', instance.getSubdir(), filename)
def validateFilterString(value):
"""
Validate that a provided filter string looks like a list of comma-separated key=value pairs
These should nominally match to a valid database filter based on the model being filtered.
e.g. "category=6, IPN=12"
e.g. "part__name=widget"
The ReportTemplate class uses the filter string to work out which items a given report applies to.
For example, an acceptance test report template might only apply to stock items with a given IPN,
so the string could be set to:
filters = "IPN = ACME0001"
Returns a map of key:value pairs
"""
# Empty results map
results = {}
value = str(value).strip()
if not value or len(value) == 0:
return results
groups = value.split(',')
for group in groups:
group = group.strip()
pair = group.split('=')
if not len(pair) == 2:
raise ValidationError(
"Invalid group: {g}".format(g=group)
)
k, v = pair
k = k.strip()
v = v.strip()
if not k or not v:
raise ValidationError(
"Invalid group: {g}".format(g=group)
)
results[k] = v
return results
class WeasyprintReportMixin(WeasyTemplateResponseMixin): class WeasyprintReportMixin(WeasyTemplateResponseMixin):
""" """
Class for rendering a HTML template to a PDF. Class for rendering a HTML template to a PDF.
@ -198,54 +147,24 @@ class ReportTemplateBase(models.Model):
description = models.CharField(max_length=250, help_text=_("Report template description")) description = models.CharField(max_length=250, help_text=_("Report template description"))
class Meta: enabled = models.BooleanField(
abstract = True default=True,
help_text=_('Report template is enabled'),
verbose_name=_('Enabled')
)
filters = models.CharField(
class ReportTemplate(ReportTemplateBase):
"""
A simple reporting template which is used to upload template files,
which can then be used in other concrete template classes.
"""
pass
class PartFilterMixin(models.Model):
"""
A model mixin used for matching a report type against a Part object.
Used to assign a report to a given part using custom filters.
"""
class Meta:
abstract = True
def matches_part(self, part):
"""
Test if this report matches a given part.
"""
filters = self.get_part_filters()
parts = PartModels.Part.objects.filter(**filters)
parts = parts.filter(pk=part.pk)
return parts.exists()
def get_part_filters(self):
""" Return a map of filters to be used for Part filtering """
return validateFilterString(self.part_filters)
part_filters = models.CharField(
blank=True, blank=True,
max_length=250, max_length=250,
help_text=_("Part query filters (comma-separated list of key=value pairs)"), help_text=_("Part query filters (comma-separated list of key=value pairs)"),
validators=[validateFilterString] validators=[validateFilterString]
) )
class Meta:
abstract = True
class TestReport(ReportTemplateBase, PartFilterMixin):
class TestReport(ReportTemplateBase):
""" """
Render a TestReport against a StockItem object. Render a TestReport against a StockItem object.
""" """
@ -256,6 +175,17 @@ class TestReport(ReportTemplateBase, PartFilterMixin):
# Requires a stock_item object to be given to it before rendering # Requires a stock_item object to be given to it before rendering
stock_item = None stock_item = None
def matches_stock_item(self, item):
"""
Test if this report template matches a given StockItem objects
"""
filters = validateFilterString(self.part_filters)
items = StockItem.objects.filter(**filters)
return items.exists()
def get_context_data(self, request): def get_context_data(self, request):
return { return {
'stock_item': self.stock_item, 'stock_item': self.stock_item,

View File

@ -1,68 +0,0 @@
"""
Performs initial setup functions.
- Generates a Django SECRET_KEY file to be used by manage.py
- Copies config template file (if a config file does not already exist)
"""
import random
import string
import os
import sys
import argparse
from shutil import copyfile
OUTPUT_DIR = os.path.dirname(os.path.realpath(__file__))
KEY_FN = 'secret_key.txt'
CONFIG_FN = 'config.yaml'
CONFIG_TEMPLATE_FN = 'config_template.yaml'
def generate_key(length=50):
""" Generate a random string
Args:
length: Number of characters in returned string (default = 50)
Returns:
Randomized secret key string
"""
options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(length)])
return key
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Generate Django SECRET_KEY file')
parser.add_argument('--force', '-f', help='Override existing files', action='store_true')
parser.add_argument('--dummy', '-d', help='Dummy run (do not create any files)', action='store_true')
args = parser.parse_args()
# Places to store files
key_filename = os.path.join(OUTPUT_DIR, KEY_FN)
conf_template = os.path.join(OUTPUT_DIR, CONFIG_TEMPLATE_FN)
conf_filename = os.path.join(OUTPUT_DIR, CONFIG_FN)
# Generate secret key data
key_data = generate_key()
if args.dummy:
print('SECRET_KEY: {k}'.format(k=key_data))
sys.exit(0)
if not args.force and os.path.exists(key_filename):
print("Key file already exists - '{f}'".format(f=key_filename))
else:
with open(key_filename, 'w') as key_file:
print("Generating SECRET_KEY file - '{f}'".format(f=key_filename))
key_file.write(key_data)
if not args.force and os.path.exists(conf_filename):
print("Config file already exists (skipping)")
else:
print("Copying config template to 'config.yaml'")
copyfile(conf_template, conf_filename)

View File

@ -13,7 +13,7 @@ from .models import StockItemTracking
from .models import StockItemTestResult from .models import StockItemTestResult
from build.models import Build from build.models import Build
from company.models import SupplierPart from company.models import Company, SupplierPart
from order.models import PurchaseOrder, SalesOrder from order.models import PurchaseOrder, SalesOrder
from part.models import Part from part.models import Part
@ -59,12 +59,14 @@ class StockItemResource(ModelResource):
# Custom manaegrs for ForeignKey fields # Custom manaegrs for ForeignKey fields
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
part_name = Field(attribute='part__full_ame', readonly=True) part_name = Field(attribute='part__full_name', readonly=True)
supplier_part = Field(attribute='supplier_part', widget=widgets.ForeignKeyWidget(SupplierPart)) supplier_part = Field(attribute='supplier_part', widget=widgets.ForeignKeyWidget(SupplierPart))
supplier = Field(attribute='supplier_part__supplier__id', readonly=True) supplier = Field(attribute='supplier_part__supplier__id', readonly=True)
customer = Field(attribute='customer', widget=widgets.ForeignKeyWidget(Company))
supplier_name = Field(attribute='supplier_part__supplier__name', readonly=True) supplier_name = Field(attribute='supplier_part__supplier__name', readonly=True)
status_label = Field(attribute='status_label', readonly=True) status_label = Field(attribute='status_label', readonly=True)
@ -77,6 +79,8 @@ class StockItemResource(ModelResource):
build = Field(attribute='build', widget=widgets.ForeignKeyWidget(Build)) build = Field(attribute='build', widget=widgets.ForeignKeyWidget(Build))
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockItem))
sales_order = Field(attribute='sales_order', widget=widgets.ForeignKeyWidget(SalesOrder)) sales_order = Field(attribute='sales_order', widget=widgets.ForeignKeyWidget(SalesOrder))
build_order = Field(attribute='build_order', widget=widgets.ForeignKeyWidget(Build)) build_order = Field(attribute='build_order', widget=widgets.ForeignKeyWidget(Build))
@ -101,6 +105,11 @@ class StockItemResource(ModelResource):
report_skipped = False report_skipped = False
clean_model_instance = True clean_model_instance = True
exclude = [
# Exclude MPTT internal model fields
'lft', 'rght', 'tree_id', 'level',
]
class StockItemAdmin(ImportExportModelAdmin): class StockItemAdmin(ImportExportModelAdmin):

View File

@ -15,6 +15,8 @@ from InvenTree.helpers import GetExportFormats
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from report.models import TestReport
from .models import StockLocation, StockItem, StockItemTracking from .models import StockLocation, StockItem, StockItemTracking
from .models import StockItemAttachment from .models import StockItemAttachment
from .models import StockItemTestResult from .models import StockItemTestResult
@ -225,12 +227,17 @@ class TestReportFormatForm(HelperForm):
self.fields['template'].choices = self.get_template_choices() self.fields['template'].choices = self.get_template_choices()
def get_template_choices(self): def get_template_choices(self):
""" Available choices """ """
Generate a list of of TestReport options for the StockItem
"""
choices = [] choices = []
for report in self.stock_item.part.get_test_report_templates(): templates = TestReport.objects.filter(enabled=True)
choices.append((report.pk, report))
for template in templates:
if template.matches_stock_item(self.stock_item):
choices.append(template)
return choices return choices

View File

@ -124,11 +124,9 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
{% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'> <button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'>
<span class='fas fa-file-invoice'/> <span class='fas fa-file-invoice'/>
</button> </button>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
@ -303,7 +301,6 @@ $("#stock-serialize").click(function() {
); );
}); });
{% if item.part.has_test_report_templates %}
$("#stock-test-report").click(function() { $("#stock-test-report").click(function() {
launchModalForm( launchModalForm(
"{% url 'stock-item-test-report-select' item.id %}", "{% url 'stock-item-test-report-select' item.id %}",
@ -312,7 +309,6 @@ $("#stock-test-report").click(function() {
} }
); );
}); });
{% endif %}
$("#print-label").click(function() { $("#print-label").click(function() {
launchModalForm( launchModalForm(

View File

@ -17,9 +17,7 @@
<button type='button' class='btn btn-danger' id='delete-test-results'>{% trans "Delete Test Data" %}</button> <button type='button' class='btn btn-danger' id='delete-test-results'>{% trans "Delete Test Data" %}</button>
{% endif %} {% endif %}
<button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Data" %}</button> <button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Data" %}</button>
{% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='test-report'>{% trans "Test Report" %} <span class='fas fa-tasks'></span></button> <button type='button' class='btn btn-default' id='test-report'>{% trans "Test Report" %} <span class='fas fa-tasks'></span></button>
{% endif %}
</div> </div>
<div class='filter-list' id='filter-list-stocktests'> <div class='filter-list' id='filter-list-stocktests'>
<!-- Empty div --> <!-- Empty div -->

View File

@ -32,7 +32,6 @@
<input class='numberinput' <input class='numberinput'
min='0' min='0'
{% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %} {% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
{% if item.serialized %} disabled='true' title='{% trans "Stock item is serialized and quantity cannot be adjusted" %}' {% endif %}
value='{% decimal item.new_quantity %}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/> value='{% decimal item.new_quantity %}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
{% if item.error %} {% if item.error %}
<br><span class='help-inline'>{{ item.error }}</span> <br><span class='help-inline'>{{ item.error }}</span>

View File

@ -310,7 +310,8 @@ class StockItemSelectLabels(AjaxView):
labels = [] labels = []
for label in StockItemLabel.objects.all(): # Construct a list of StockItemLabel objects which are enabled, and the filters match the selected StockItem
for label in StockItemLabel.objects.filter(enabled=True):
if label.matches_stock_item(item): if label.matches_stock_item(item):
labels.append(label) labels.append(label)
@ -1119,6 +1120,7 @@ class StockItemSerialize(AjaxUpdateView):
initials['quantity'] = item.quantity initials['quantity'] = item.quantity
initials['serial_numbers'] = item.part.getSerialNumberString(item.quantity) initials['serial_numbers'] = item.part.getSerialNumberString(item.quantity)
if item.location is not None:
initials['destination'] = item.location.pk initials['destination'] = item.location.pk
return initials return initials

View File

@ -1,11 +1,19 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<nav class="navbar navbar-xs navbar-default navbar-fixed-top ">
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header clearfix content-heading"> <div class="navbar-header clearfix content-heading">
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a> <a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a>
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div> </div>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span>{% trans "Parts" %}</a></li> <li><a href="{% url 'part-index' %}"><span class='fas fa-shapes icon-header'></span>{% trans "Parts" %}</a></li>
<li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li> <li><a href="{% url 'stock-index' %}"><span class='fas fa-boxes icon-header'></span>{% trans "Stock" %}</a></li>
@ -53,4 +61,5 @@
</li> </li>
</ul> </ul>
</div> </div>
</div>
</nav> </nav>

View File

@ -1,75 +0,0 @@
clean:
find . -path '*/__pycache__/*' -delete
find . -type d -name '__pycache__' -empty -delete
find . -name *.pyc -o -name *.pyo -delete
rm -rf *.egg-info
rm -rf .cache
rm -rf .tox
rm -f .coverage
update: install migrate static
# Perform database migrations (after schema changes are made)
migrate:
cd InvenTree && python3 manage.py makemigrations
cd InvenTree && python3 manage.py migrate
cd InvenTree && python3 manage.py migrate --run-syncdb
cd InvenTree && python3 manage.py check
# Collect static files into the correct locations
static:
cd InvenTree && python3 manage.py collectstatic
# Install all required packages
install:
pip3 install -U -r requirements.txt
cd InvenTree && python3 setup.py
# Create a superuser account
superuser:
cd InvenTree && python3 manage.py createsuperuser
# Install pre-requisites for mysql setup
mysql:
sudo apt-get install mysql-server libmysqlclient-dev
pip3 install mysqlclient
# Install pre-requisites for postgresql setup
postgresql:
sudo apt-get install postgresql postgresql-contrib libpq-dev
pip3 install psycopg2
# Update translation files
translate:
cd InvenTree && python3 manage.py makemessages
cd InvenTree && python3 manage.py compilemessages
# Run PEP style checks against source code
style:
flake8 InvenTree
# Run unit tests
test:
cd InvenTree && python3 manage.py check
cd InvenTree && python3 manage.py test barcode build common company label order part report stock InvenTree
# Run code coverage
coverage:
cd InvenTree && python3 manage.py check
coverage run InvenTree/manage.py test barcode build common company label order part report stock InvenTree
coverage html
# Install packages required to generate code docs
docreqs:
pip3 install -U -r docs/requirements.txt
# Build code docs
docs:
cd docs && make html
# Make database backup
backup:
cd InvenTree && python3 manage.py dbbackup
cd InvenTree && python3 manage.py mediabackup
.PHONY: clean migrate superuser install mysql postgresql translate static style test coverage docreqs docs backup update

View File

@ -17,7 +17,7 @@ django-import-export==2.0.0 # Data import / export for admin interface
django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files
django-qr-code==1.2.0 # Generate QR codes django-qr-code==1.2.0 # Generate QR codes
flake8==3.8.3 # PEP checking flake8==3.8.3 # PEP checking
coverage==4.0.3 # Unit test coverage coverage==5.2.1 # Unit test coverage
python-coveralls==2.9.1 # Coveralls linking (for Travis) python-coveralls==2.9.1 # Coveralls linking (for Travis)
rapidfuzz==0.7.6 # Fuzzy string matching rapidfuzz==0.7.6 # Fuzzy string matching
django-stdimage==5.1.1 # Advanced ImageField management django-stdimage==5.1.1 # Advanced ImageField management

254
tasks.py Normal file
View File

@ -0,0 +1,254 @@
# -*- coding: utf-8 -*-
from invoke import task
from shutil import copyfile
import random
import string
import os
def apps():
"""
Returns a list of installed apps
"""
return [
'barcode',
'build',
'common',
'company',
'label',
'order',
'part',
'report',
'stock',
'InvenTree'
]
def localDir():
"""
Returns the directory of *THIS* file.
Used to ensure that the various scripts always run
in the correct directory.
"""
return os.path.dirname(os.path.abspath(__file__))
def managePyDir():
"""
Returns the directory of the manage.py file
"""
return os.path.join(localDir(), 'InvenTree')
def managePyPath():
"""
Return the path of the manage.py file
"""
return os.path.join(managePyDir(), 'manage.py')
def manage(c, cmd):
"""
Runs a given command against django's "manage.py" script.
Args:
c - Command line context
cmd - django command to run
"""
c.run('cd {path} && python3 manage.py {cmd}'.format(
path=managePyDir(),
cmd=cmd
))
@task(help={'length': 'Length of secret key (default=50)'})
def key(c, length=50, force=False):
"""
Generates a SECRET_KEY file which InvenTree uses for generating security hashes
"""
SECRET_KEY_FILE = os.path.join(localDir(), 'InvenTree', 'secret_key.txt')
# If a SECRET_KEY file does not exist, generate a new one!
if force or not os.path.exists(SECRET_KEY_FILE):
print("Generating SECRET_KEY file - " + SECRET_KEY_FILE)
with open(SECRET_KEY_FILE, 'w') as key_file:
options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(length)])
key_file.write(key)
else:
print("SECRET_KEY file already exists - skipping")
@task(post=[key])
def install(c):
"""
Installs required python packages, and runs initial setup functions.
"""
# Install required Python packages with PIP
c.run('pip3 install -U -r requirements.txt')
# If a config.yaml file does not exist, copy from the template!
CONFIG_FILE = os.path.join(localDir(), 'InvenTree', 'config.yaml')
CONFIG_TEMPLATE_FILE = os.path.join(localDir(), 'InvenTree', 'config_template.yaml')
if not os.path.exists(CONFIG_FILE):
print("Config file 'config.yaml' does not exist - copying from template.")
copyfile(CONFIG_TEMPLATE_FILE, CONFIG_FILE)
@task
def superuser(c):
"""
Create a superuser (admin) account for the database.
"""
manage(c, 'createsuperuser')
@task
def migrate(c):
"""
Performs database migrations.
This is a critical step if the database schema have been altered!
"""
print("Running InvenTree database migrations...")
print("========================================")
manage(c, "makemigrations")
manage(c, "migrate")
manage(c, "migrate --run-syncdb")
manage(c, "check")
print("========================================")
print("InvenTree database migrations completed!")
@task
def static(c):
"""
Copies required static files to the STATIC_ROOT directory,
as per Django requirements.
"""
manage(c, "collectstatic")
@task(pre=[install, migrate, static])
def update(c):
"""
Update InvenTree installation.
This command should be invoked after source code has been updated,
e.g. downloading new code from GitHub.
The following tasks are performed, in order:
- install
- migrate
- static
"""
pass
@task
def translate(c):
"""
Regenerate translation files.
Run this command after added new translatable strings,
or after adding translations for existing strings.
"""
manage(c, "makemigrations")
manage(c, "compilemessages")
@task
def style(c):
"""
Run PEP style checks against InvenTree sourcecode
"""
print("Running PEP style checks...")
c.run('flake8 InvenTree')
@task
def test(c):
"""
Run unit-tests for InvenTree codebase.
"""
# Run sanity check on the django install
manage(c, 'check')
# Run coverage tests
manage(c, 'test {apps}'.format(
apps=' '.join(apps())
))
@task
def coverage(c):
"""
Run code-coverage of the InvenTree codebase,
using the 'coverage' code-analysis tools.
Generates a code coverage report (available in the htmlcov directory)
"""
# Run sanity check on the django install
manage(c, 'check')
# Run coverage tests
c.run('coverage run {manage} test {apps}'.format(
manage=managePyPath(),
apps=' '.join(apps())
))
# Generate coverage report
c.run('coverage html')
@task
def mysql(c):
"""
Install packages required for using InvenTree with a MySQL database.
"""
print('Installing packages required for MySQL')
c.run('sudo apt-get install mysql-server libmysqlclient-dev')
c.run('pip3 install mysqlclient')
@task
def postgresql(c):
"""
Install packages required for using InvenTree with a PostgreSQL database
"""
print("Installing packages required for PostgreSQL")
c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev')
c.run('pip3 install psycopg2')
@task
def backup(c):
"""
Create a backup of database models and uploaded media files.
Backup files will be written to the 'backup_dir' file specified in 'config.yaml'
"""
manage(c, 'dbbackup')
manage(c, 'mediabackup')
@task(help={'address': 'Server address:port (default=127.0.0.1:8000)'})
def server(c, address="127.0.0.1:8000"):
"""
Launch a (deveopment) server using Django's in-built webserver.
Note: This is *not* sufficient for a production installation.
"""
manage(c, "runserver {address}".format(address=address))