From 187de59e3b15729a4c47a721d5a4a151a33de8cf Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 28 Mar 2017 23:42:31 +1100 Subject: [PATCH 01/42] Added build-status picture --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index df916cf918..b016b68fd8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # InvenTree Open Source Inventory Management System +[![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) ## Documentation For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated) From 79b7c7d75482dfb69884b544c16de17f556f4f90 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 28 Mar 2017 23:42:40 +1100 Subject: [PATCH 02/42] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b016b68fd8..7db084f6dc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # InvenTree Open Source Inventory Management System + [![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) ## Documentation From 68ae1110adb0e620038485375fe111ff0cb50d3e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 08:37:41 +1100 Subject: [PATCH 03/42] Updates - Improved some models - Added PEP check script --- .travis.yml | 2 +- InvenTree/InvenTree/models.py | 4 ++-- InvenTree/part/models.py | 28 ++++++++++++++-------------- InvenTree/part/serializers.py | 17 +++++++++++++++-- InvenTree/stock/models.py | 5 ++++- pep_check.py | 3 +++ 6 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 pep_check.py diff --git a/.travis.yml b/.travis.yml index 2ee970d740..3f55eeb22b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ before_install: - pip install djangorestframework script: - - "pep8 --exclude=migrations --ignore=E402,W293,E501 InvenTree" + - python pep_check.py - python InvenTree/manage.py check - python InvenTree/manage.py test --noinput \ No newline at end of file diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 81bf2c62db..e28ca80111 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -38,7 +38,7 @@ class InvenTreeTree(models.Model): abstract = True name = models.CharField(max_length=100) - description = models.CharField(max_length=250) + description = models.CharField(max_length=250, blank=True) parent = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, @@ -83,7 +83,7 @@ class InvenTreeTree(models.Model): for a in available: if a.id not in childs: acceptable.append(a) - + return acceptable @property diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 863d2c44a4..62041f756b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -39,20 +39,13 @@ class Part(models.Model): verbose_name = "Part" verbose_name_plural = "Parts" - @property - def stock_list(self): - """ Return a list of all stock objects associated with this part - """ - - return self.stockitem_set.all() - @property def stock(self): """ Return the total stock quantity for this part. Part may be stored in multiple locations """ - stocks = self.stock_list + stocks = self.locations.all() if len(stocks) == 0: return 0 @@ -132,8 +125,7 @@ class PartParameter(models.Model): """ PartParameter is associated with a single part """ - part = models.ForeignKey(Part, on_delete=models.CASCADE) - + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') template = models.ForeignKey(PartParameterTemplate) # Value data @@ -145,10 +137,10 @@ class PartParameter(models.Model): # from being added to the same part def save(self, *args, **kwargs): params = PartParameter.objects.filter(part=self.part, template=self.template) - if len(params) > 0: - raise ValidationError("Parameter '{param}' already exists for {part}".format( - param=self.template.name, - part=self.part.name)) + if len(params) > 1: + return + if len(params) == 1 and params[0].id != self.id: + return super(PartParameter, self).save(*args, **kwargs) @@ -158,6 +150,14 @@ class PartParameter(models.Model): val=self.value, units=self.template.units) + @property + def units(self): + return self.template.units + + @property + def name(self): + return self.template.name + class Meta: verbose_name = "Part Parameter" verbose_name_plural = "Part Parameters" diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index b15ea800c6..267db2b40c 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -1,16 +1,29 @@ from rest_framework import serializers -from .models import Part, PartCategory +from .models import Part, PartCategory, PartParameter + + +class ParameterSerializer(serializers.ModelSerializer): + class Meta: + model = PartParameter + fields = ('name', + 'value', + 'units') class PartSerializer(serializers.ModelSerializer): + + params = ParameterSerializer(source='parameters', many=True) + class Meta: model = Part fields = ('pk', + 'name', 'IPN', 'description', 'category', - 'stock') + 'stock', + 'params') class PartCategorySerializer(serializers.ModelSerializer): diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f72a0f3d3a..0e6baeb840 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -12,13 +12,15 @@ class Warehouse(InvenTreeTree): class StockItem(models.Model): part = models.ForeignKey(Part, - on_delete=models.CASCADE) + on_delete=models.CASCADE, + related_name='locations') location = models.ForeignKey(Warehouse, on_delete=models.CASCADE) quantity = models.IntegerField() updated = models.DateField(auto_now=True) # Stock status types ITEM_IN_PROGRESS = 0 + ITEM_INCOMING = 5 ITEM_DAMAGED = 10 ITEM_ATTENTION = 20 ITEM_COMPLETE = 50 @@ -26,6 +28,7 @@ class StockItem(models.Model): status = models.IntegerField(default=ITEM_IN_PROGRESS, choices=[ (ITEM_IN_PROGRESS, "In progress"), + (ITEM_INCOMING, "Incoming"), (ITEM_DAMAGED, "Damaged"), (ITEM_ATTENTION, "Requires attention"), (ITEM_COMPLETE, "Complete") diff --git a/pep_check.py b/pep_check.py new file mode 100644 index 0000000000..64de2a568f --- /dev/null +++ b/pep_check.py @@ -0,0 +1,3 @@ +import subprocess + +subprocess.call(['pep8', '--exclude=migrations', '--ignore=EC04,W293,E501', 'InvenTree']) \ No newline at end of file From 9b0b5d9be1cb3781671dc826d3781d21bede3288 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 29 Mar 2017 11:58:34 +1100 Subject: [PATCH 04/42] Added installation directions --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 7db084f6dc..fdf28011fb 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,16 @@ Open Source Inventory Management System [![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) +## Installation +When first installing InvenTree, initial database configuration must be performed. This is handled by the `install.py` script, which performs the following actions: + +1. Installs required django packages (requires [pip](https://pypi.python.org/pypi/pip)) +1. Performs initial database setup +1. Updates database tables for all InvenTree components + +This script can also be used to update the installation if changes have been made to the database configuration. + +To create an initial user account, run the command `python InvenTree/manage.py createsuperuser` + ## Documentation For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated) From 2b998a1931e9d318952e5809e05bc8e922f1efc9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 20:23:21 +1100 Subject: [PATCH 05/42] Added function to get all parents for a tree item --- InvenTree/InvenTree/models.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index e28ca80111..7e06b1a183 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -42,7 +42,23 @@ class InvenTreeTree(models.Model): parent = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, - null=True) + null=True, + related_name='children') + + def getUniqueParents(self, unique=None): + """ Return a flat set of all parent items that exist above this node. + If any parents are repeated (which would be very bad!), the process is halted + """ + + if unique is None: + unique = set() + else: + unique.add(self.id) + + if self.parent and self.parent.id not in unique: + self.parent.getUniqueParents(unique) + + return unique def getUniqueChildren(self, unique=None): """ Return a flat set of all child items that exist under this node. @@ -60,9 +76,6 @@ class InvenTreeTree(models.Model): # Some magic to get around the limitations of abstract models contents = ContentType.objects.get_for_model(type(self)) children = contents.get_all_objects_for_this_type(parent=self.id) - - for child in children: - child.getUniqueChildren(unique) return unique From aafa8781d72bae941406c173f15190e316831a48 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:44:59 +1100 Subject: [PATCH 06/42] Added "ProjectRun" model --- InvenTree/project/models.py | 32 +++++++++++++++++++++++--------- InvenTree/supplier/models.py | 15 ++++++++++----- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index a46b641d39..2e228314ae 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -11,7 +11,7 @@ class ProjectCategory(InvenTreeTree): Each ProjectCategory can contain zero-or-more child categories, and in turn can have zero-or-one parent category. """ - + class Meta: verbose_name = "Project Category" verbose_name_plural = "Project Categories" @@ -21,14 +21,14 @@ class Project(models.Model): """ A Project takes multiple Part objects. A project can output zero-or-more Part objects """ - + name = models.CharField(max_length=100) description = models.CharField(max_length=500, blank=True) category = models.ForeignKey(ProjectCategory, on_delete=models.CASCADE) - + def __str__(self): return self.name - + @property def projectParts(self): """ Return a list of all project parts associated with this project @@ -41,23 +41,37 @@ class ProjectPart(models.Model): The quantity of parts required for a single-run of that project is stored. The overage is the number of extra parts that are generally used for a single run. """ - + # Overage types OVERAGE_PERCENT = 0 OVERAGE_ABSOLUTE = 1 - + part = models.ForeignKey(Part, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE) - quantity = models.IntegerField(default=1) + quantity = models.PositiveIntegerField(default=1) overage = models.FloatField(default=0) - overage_type = models.IntegerField( + overage_type = models.PositiveIntegerField( default=1, choices=[ (OVERAGE_PERCENT, "Percent"), (OVERAGE_ABSOLUTE, "Absolute") ]) - + def __str__(self): return "{quan} x {name}".format( name=self.part.name, quan=self.quantity) + +class ProjectRun(models.Model): + """ A single run of a particular project. + Tracks the number of 'units' made in the project. + Provides functionality to update stock, + based on both: + a) Parts used (project inputs) + b) Parts produced (project outputs) + """ + + project = models.ForeignKey(Project, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + + run_date = models.DateField(auto_now_add=True) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index db4f8a6659..968f2d011c 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -9,9 +9,9 @@ from part.models import Part class Supplier(Company): """ Represents a manufacturer or supplier """ - + pass - + class Manufacturer(Company): """ Represents a manfufacturer @@ -37,13 +37,18 @@ class SupplierPart(models.Model): supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) SKU = models.CharField(max_length=100) - + manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.CASCADE) MPN = models.CharField(max_length=100, blank=True) - + URL = models.URLField(blank=True) description = models.CharField(max_length=250, blank=True) + single_price = models.DecimalField(max_digits=10, decimal_places=3) + + # packaging that the part is supplied in, e.g. "Reel" + packaging = models.CharField(max_length=50, blank=True) + def __str__(self): return "{mpn} - {supplier}".format( mpn=self.MPN, @@ -58,7 +63,7 @@ class SupplierPriceBreak(models.Model): part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE) - quantity = models.IntegerField() + quantity = models.PositiveIntegerField() cost = models.DecimalField(max_digits=10, decimal_places=3) currency = models.CharField(max_length=10, blank=True) From 591ae5dc5c2937049fa15102af6a40b5dd06e279 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:47:17 +1100 Subject: [PATCH 07/42] Single price field for supplier part --- InvenTree/supplier/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 968f2d011c..cb5859f432 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -44,7 +44,9 @@ class SupplierPart(models.Model): URL = models.URLField(blank=True) description = models.CharField(max_length=250, blank=True) - single_price = models.DecimalField(max_digits=10, decimal_places=3) + single_price = models.DecimalField(max_digits=10, + decimal_places=3, + default=0) # packaging that the part is supplied in, e.g. "Reel" packaging = models.CharField(max_length=50, blank=True) From 2355adb0441f470b7b74dd7354c6088c069c4f47 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:55:16 +1100 Subject: [PATCH 08/42] Added lead-time for supplier part --- InvenTree/supplier/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index cb5859f432..8a7d00d2c0 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -51,6 +51,9 @@ class SupplierPart(models.Model): # packaging that the part is supplied in, e.g. "Reel" packaging = models.CharField(max_length=50, blank=True) + # lead time for parts that cannot be delivered immediately + lead_time = models.DurationField(blank=True, null=True) + def __str__(self): return "{mpn} - {supplier}".format( mpn=self.MPN, From b2eca2aa48cab10d2b80131ae0421097272d153d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 22:55:28 +1100 Subject: [PATCH 09/42] Added ETA for part stock --- InvenTree/project/models.py | 1 + InvenTree/stock/models.py | 30 +++++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index 2e228314ae..2878c87497 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -62,6 +62,7 @@ class ProjectPart(models.Model): name=self.part.name, quan=self.quantity) + class ProjectRun(models.Model): """ A single run of a particular project. Tracks the number of 'units' made in the project. diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 0e6baeb840..51e3ca2b3b 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -8,32 +8,36 @@ from InvenTree.models import InvenTreeTree class Warehouse(InvenTreeTree): pass - + class StockItem(models.Model): part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='locations') location = models.ForeignKey(Warehouse, on_delete=models.CASCADE) - quantity = models.IntegerField() + quantity = models.PositiveIntegerField() updated = models.DateField(auto_now=True) - + # Stock status types ITEM_IN_PROGRESS = 0 ITEM_INCOMING = 5 ITEM_DAMAGED = 10 ITEM_ATTENTION = 20 ITEM_COMPLETE = 50 - - status = models.IntegerField(default=ITEM_IN_PROGRESS, - choices=[ - (ITEM_IN_PROGRESS, "In progress"), - (ITEM_INCOMING, "Incoming"), - (ITEM_DAMAGED, "Damaged"), - (ITEM_ATTENTION, "Requires attention"), - (ITEM_COMPLETE, "Complete") - ]) - + + status = models.PositiveIntegerField( + default=ITEM_IN_PROGRESS, + choices=[ + (ITEM_IN_PROGRESS, "In progress"), + (ITEM_INCOMING, "Incoming"), + (ITEM_DAMAGED, "Damaged"), + (ITEM_ATTENTION, "Requires attention"), + (ITEM_COMPLETE, "Complete") + ]) + + # If stock item is incoming, an (optional) ETA field + expected_arrival = models.DateField(null=True, blank=True) + def __str__(self): return "{n} x {part} @ {loc}".format( n=self.quantity, From 8ba4ea344e515451d7cdcbb01fba869044bd3745 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:19:53 +1100 Subject: [PATCH 10/42] updates --- InvenTree/part/models.py | 79 +++++++++++++++++++----------------- InvenTree/project/admin.py | 7 +++- InvenTree/project/models.py | 11 +++-- InvenTree/stock/models.py | 34 +++++++++------- InvenTree/supplier/models.py | 6 +-- 5 files changed, 77 insertions(+), 60 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 62041f756b..e9d246fd46 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals - +from django.utils.translation import ugettext as _ from django.db import models from django.db.models import Sum from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -10,15 +10,15 @@ from InvenTree.models import InvenTreeTree class PartCategory(InvenTreeTree): """ PartCategory provides hierarchical organization of Part objects. """ - + class Meta: verbose_name = "Part Category" verbose_name_plural = "Part Categories" - - + + class Part(models.Model): """ Represents a """ - + name = models.CharField(max_length=100) description = models.CharField(max_length=250, blank=True) IPN = models.CharField(max_length=100, blank=True) @@ -26,7 +26,7 @@ class Part(models.Model): minimum_stock = models.IntegerField(default=0) units = models.CharField(max_length=20, default="pcs", blank=True) trackable = models.BooleanField(default=False) - + def __str__(self): if self.IPN: return "{name} ({ipn})".format( @@ -34,39 +34,39 @@ class Part(models.Model): name=self.name) else: return self.name - + class Meta: verbose_name = "Part" verbose_name_plural = "Parts" - + @property def stock(self): """ Return the total stock quantity for this part. Part may be stored in multiple locations """ - + stocks = self.locations.all() if len(stocks) == 0: return 0 - + result = stocks.aggregate(total=Sum('quantity')) return result['total'] - + @property def projects(self): """ Return a list of unique projects that this part is associated with """ - + project_ids = set() project_parts = self.projectpart_set.all() - + projects = [] - + for pp in project_parts: if pp.project.id not in project_ids: project_ids.add(pp.project.id) projects.append(pp.project) - + return projects @@ -78,28 +78,31 @@ class PartParameterTemplate(models.Model): name = models.CharField(max_length=20) description = models.CharField(max_length=100, blank=True) units = models.CharField(max_length=10, blank=True) - + default_value = models.CharField(max_length=50, blank=True) default_min = models.CharField(max_length=50, blank=True) default_max = models.CharField(max_length=50, blank=True) - + # Parameter format PARAM_NUMERIC = 10 PARAM_TEXT = 20 PARAM_BOOL = 30 - + + PARAM_TYPE_CODES = { + PARAM_NUMERIC: _("Numeric"), + PARAM_TEXT: _("Text"), + PARAM_BOOL: _("Bool") + } + format = models.IntegerField( default=PARAM_NUMERIC, - choices=[ - (PARAM_NUMERIC, "Numeric"), - (PARAM_TEXT, "Text"), - (PARAM_BOOL, "Boolean")]) - + choices=PARAM_TYPE_CODES.items()) + def __str__(self): return "{name} ({units})".format( name=self.name, units=self.units) - + class Meta: verbose_name = "Parameter Template" verbose_name_plural = "Parameter Templates" @@ -110,7 +113,7 @@ class CategoryParameterLink(models.Model): """ category = models.ForeignKey(PartCategory, on_delete=models.CASCADE) template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE) - + def __str__(self): return "{name} - {cat}".format( name=self.template.name, @@ -119,20 +122,20 @@ class CategoryParameterLink(models.Model): class Meta: verbose_name = "Category Parameter" verbose_name_plural = "Category Parameters" - + class PartParameter(models.Model): """ PartParameter is associated with a single part """ - + part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters') template = models.ForeignKey(PartParameterTemplate) - + # Value data value = models.CharField(max_length=50, blank=True) min_value = models.CharField(max_length=50, blank=True) max_value = models.CharField(max_length=50, blank=True) - + # Prevent multiple parameters of the same template # from being added to the same part def save(self, *args, **kwargs): @@ -141,27 +144,27 @@ class PartParameter(models.Model): return if len(params) == 1 and params[0].id != self.id: return - + super(PartParameter, self).save(*args, **kwargs) - + def __str__(self): return "{name} : {val}{units}".format( name=self.template.name, val=self.value, units=self.template.units) - + @property def units(self): return self.template.units - + @property def name(self): return self.template.name - + class Meta: verbose_name = "Part Parameter" verbose_name_plural = "Part Parameters" - + class PartRevision(models.Model): """ A PartRevision represents a change-notification to a Part @@ -169,12 +172,12 @@ class PartRevision(models.Model): which should be tracked. UniqueParts can have a single associated PartRevision """ - + part = models.ForeignKey(Part, on_delete=models.CASCADE) - + name = models.CharField(max_length=100) description = models.CharField(max_length=500) revision_date = models.DateField(auto_now_add=True) - + def __str__(self): return self.name diff --git a/InvenTree/project/admin.py b/InvenTree/project/admin.py index b37fa8435c..0f1ad27b11 100644 --- a/InvenTree/project/admin.py +++ b/InvenTree/project/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import ProjectCategory, Project, ProjectPart +from .models import ProjectCategory, Project, ProjectPart, ProjectRun class ProjectCategoryAdmin(admin.ModelAdmin): @@ -14,6 +14,11 @@ class ProjectAdmin(admin.ModelAdmin): class ProjectPartAdmin(admin.ModelAdmin): list_display = ('part', 'project', 'quantity') + +class ProjectRunAdmin(admin.ModelAdmin): + list_display = ('project', 'quantity', 'run_date') + admin.site.register(ProjectCategory, ProjectCategoryAdmin) admin.site.register(Project, ProjectAdmin) admin.site.register(ProjectPart, ProjectPartAdmin) +admin.site.register(ProjectRun, ProjectRunAdmin) diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index 2878c87497..15f87ae006 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from django.utils.translation import ugettext as _ from django.db import models @@ -46,16 +47,18 @@ class ProjectPart(models.Model): OVERAGE_PERCENT = 0 OVERAGE_ABSOLUTE = 1 + OVARAGE_CODES = { + OVERAGE_PERCENT: _("Percent"), + OVERAGE_ABSOLUTE: _("Absolute") + } + part = models.ForeignKey(Part, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE) quantity = models.PositiveIntegerField(default=1) overage = models.FloatField(default=0) overage_type = models.PositiveIntegerField( default=1, - choices=[ - (OVERAGE_PERCENT, "Percent"), - (OVERAGE_ABSOLUTE, "Absolute") - ]) + choices=OVARAGE_CODES.items()) def __str__(self): return "{quan} x {name}".format( diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 51e3ca2b3b..e4b8f3bcfa 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals - +from django.utils.translation import ugettext as _ from django.db import models from part.models import Part @@ -19,21 +19,27 @@ class StockItem(models.Model): updated = models.DateField(auto_now=True) # Stock status types - ITEM_IN_PROGRESS = 0 - ITEM_INCOMING = 5 - ITEM_DAMAGED = 10 - ITEM_ATTENTION = 20 - ITEM_COMPLETE = 50 + ITEM_IN_STOCK = 10 + ITEM_INCOMING = 15 + ITEM_IN_PROGRESS = 20 + ITEM_COMPLETE = 25 + ITEM_ATTENTION = 50 + ITEM_DAMAGED = 55 + ITEM_DESTROYED = 60 + + ITEM_STATUS_CODES = { + ITEM_IN_STOCK: _("In stock"), + ITEM_INCOMING: _("Incoming"), + ITEM_IN_PROGRESS: _("In progress"), + ITEM_COMPLETE: _("Complete"), + ITEM_ATTENTION: _("Attention needed"), + ITEM_DAMAGED: _("Damaged"), + ITEM_DESTROYED: _("Destroyed") + } status = models.PositiveIntegerField( - default=ITEM_IN_PROGRESS, - choices=[ - (ITEM_IN_PROGRESS, "In progress"), - (ITEM_INCOMING, "Incoming"), - (ITEM_DAMAGED, "Damaged"), - (ITEM_ATTENTION, "Requires attention"), - (ITEM_COMPLETE, "Complete") - ]) + default=ITEM_IN_STOCK, + choices=ITEM_STATUS_CODES.items()) # If stock item is incoming, an (optional) ETA field expected_arrival = models.DateField(null=True, blank=True) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 8a7d00d2c0..36c164afbc 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -32,7 +32,7 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part, + part = models.ForeignKey(Part,null=True,blank=True, on_delete=models.CASCADE) supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) @@ -55,8 +55,8 @@ class SupplierPart(models.Model): lead_time = models.DurationField(blank=True, null=True) def __str__(self): - return "{mpn} - {supplier}".format( - mpn=self.MPN, + return "{sku} - {supplier}".format( + sku=self.SKU, supplier=self.supplier.name) From d0489d692aef75a405172df35da4e773d885bfe6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:21:54 +1100 Subject: [PATCH 11/42] PEP fixes --- InvenTree/supplier/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 36c164afbc..0da61c150e 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -32,10 +32,8 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part,null=True,blank=True, - on_delete=models.CASCADE) - supplier = models.ForeignKey(Supplier, - on_delete=models.CASCADE) + part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE) + supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) SKU = models.CharField(max_length=100) manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.CASCADE) From 9db1f99e26fcada5fa4d2c1716361ef6ff4cd7a3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:32:21 +1100 Subject: [PATCH 12/42] updated supplier model --- InvenTree/supplier/models.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index 0da61c150e..b288be7c2c 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -9,7 +9,6 @@ from part.models import Part class Supplier(Company): """ Represents a manufacturer or supplier """ - pass @@ -32,7 +31,7 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE) + part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE, related_name='supplier_parts') supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) SKU = models.CharField(max_length=100) @@ -49,6 +48,9 @@ class SupplierPart(models.Model): # packaging that the part is supplied in, e.g. "Reel" packaging = models.CharField(max_length=50, blank=True) + # multiple that the part is provided in + multiple = models.PositiveIntegerField(default=1) + # lead time for parts that cannot be delivered immediately lead_time = models.DurationField(blank=True, null=True) @@ -64,12 +66,9 @@ class SupplierPriceBreak(models.Model): - SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s) """ - part = models.ForeignKey(SupplierPart, - on_delete=models.CASCADE) + part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='price_breaks') quantity = models.PositiveIntegerField() cost = models.DecimalField(max_digits=10, decimal_places=3) - currency = models.CharField(max_length=10, - blank=True) def __str__(self): return "{mpn} - {cost}{currency} @ {quan}".format( From 6e0a02a885b1f494ed399a7c85c50f38d53e5807 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:36:06 +1100 Subject: [PATCH 13/42] Added extra fields to stock item --- InvenTree/stock/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index e4b8f3bcfa..40d95a5676 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -18,6 +18,11 @@ class StockItem(models.Model): quantity = models.PositiveIntegerField() updated = models.DateField(auto_now=True) + # last time the stock was checked / counted + last_checked = models.DateField(blank=True, null=True) + + review_needed = models.BooleanField(default=False) + # Stock status types ITEM_IN_STOCK = 10 ITEM_INCOMING = 15 From 4046c6f0bf5229a442e5541180954891d7534fc3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 Mar 2017 23:45:27 +1100 Subject: [PATCH 14/42] Added gitattributes file --- .gitattributes | 6 ++++++ InvenTree/part/serializers.py | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..db355084a6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +* text=auto + +*.py text +*.md text +*.html text +*.txt text diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 267db2b40c..9dea0f0231 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -12,9 +12,9 @@ class ParameterSerializer(serializers.ModelSerializer): class PartSerializer(serializers.ModelSerializer): - + params = ParameterSerializer(source='parameters', many=True) - + class Meta: model = Part fields = ('pk', @@ -24,12 +24,13 @@ class PartSerializer(serializers.ModelSerializer): 'category', 'stock', 'params') - - + + class PartCategorySerializer(serializers.ModelSerializer): class Meta: model = PartCategory fields = ('pk', 'name', 'description', + 'parent', 'path') From 79da23988319952c70db4549199535abd7c87b62 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 29 Mar 2017 23:51:12 +1100 Subject: [PATCH 15/42] Rename LICENSE to LICENSE2 --- LICENSE => LICENSE2 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE2 (100%) diff --git a/LICENSE b/LICENSE2 similarity index 100% rename from LICENSE rename to LICENSE2 From 1d57e4c5bb31fbc5c10fc42cd604b8ad79b434cb Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 29 Mar 2017 23:53:52 +1100 Subject: [PATCH 16/42] Rename LICENSE2 to LICENSE --- LICENSE2 => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE2 => LICENSE (100%) diff --git a/LICENSE2 b/LICENSE similarity index 100% rename from LICENSE2 rename to LICENSE From 1a8944519acc8d4fe97116aadfc087657c462b6b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 30 Mar 2017 08:48:52 +1100 Subject: [PATCH 17/42] Added extra fields to supplierpart --- InvenTree/supplier/models.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index b288be7c2c..bbd0f2df3f 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -31,7 +31,7 @@ class SupplierPart(models.Model): - A Part may be available from multiple suppliers """ - part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE, related_name='supplier_parts') + part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE) supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE) SKU = models.CharField(max_length=100) @@ -41,9 +41,11 @@ class SupplierPart(models.Model): URL = models.URLField(blank=True) description = models.CharField(max_length=250, blank=True) - single_price = models.DecimalField(max_digits=10, - decimal_places=3, - default=0) + # Default price for a single unit + single_price = models.DecimalField(max_digits=10, decimal_places=3, default=0) + + # Base charge added to order independent of quantity e.g. "Reeling Fee" + base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0) # packaging that the part is supplied in, e.g. "Reel" packaging = models.CharField(max_length=50, blank=True) @@ -51,6 +53,9 @@ class SupplierPart(models.Model): # multiple that the part is provided in multiple = models.PositiveIntegerField(default=1) + # Mimumum number required to order + minimum = models.PositiveIntegerField(default=1) + # lead time for parts that cannot be delivered immediately lead_time = models.DurationField(blank=True, null=True) From 0151eb18daf911629b3cbdeaea8250bbedf125cc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 30 Mar 2017 08:52:54 +1100 Subject: [PATCH 18/42] Improvements for unique part --- InvenTree/track/admin.py | 2 +- InvenTree/track/models.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/InvenTree/track/admin.py b/InvenTree/track/admin.py index ac6cf4ca7f..17a3aa6cb2 100644 --- a/InvenTree/track/admin.py +++ b/InvenTree/track/admin.py @@ -4,6 +4,6 @@ from .models import UniquePart class UniquePartAdmin(admin.ModelAdmin): - list_display = ('part', 'revision', 'serial', 'creation_date') + list_display = ('part', 'revision', 'serial', 'status', 'creation_date') admin.site.register(UniquePart, UniquePartAdmin) diff --git a/InvenTree/track/models.py b/InvenTree/track/models.py index 03d159c31e..4fed0fdd60 100644 --- a/InvenTree/track/models.py +++ b/InvenTree/track/models.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals - +from django.utils.translation import ugettext as _ from django.db import models from django.contrib.auth.models import User @@ -36,15 +36,16 @@ class UniquePart(models.Model): PART_DAMAGED = 40 PART_DESTROYED = 50 - status = models.IntegerField(default=PART_IN_PROGRESS, - choices=[ - (PART_IN_PROGRESS, "In progress"), - (PART_IN_STOCK, "In stock"), - (PART_SHIPPED, "Shipped"), - (PART_RETURNED, "Returned"), - (PART_DAMAGED, "Damaged"), - (PART_DESTROYED, "Destroyed"), - ]) + PART_STATUS_CODES = { + PART_IN_PROGRESS: _("In progress"), + PART_IN_STOCK: _("In stock"), + PART_SHIPPED: _("Shipped"), + PART_RETURNED: _("Returned"), + PART_DAMAGED: _("Damaged"), + PART_DESTROYED: _("Destroyed") + } + + status = models.IntegerField(default=PART_IN_PROGRESS, choices=PART_STATUS_CODES.items()) def __str__(self): return self.part.name From 0df5093440798640471a72f1f2679d6f26062599 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 1 Apr 2017 13:17:17 +1100 Subject: [PATCH 19/42] Added docstring for pep checker --- pep_check.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pep_check.py b/pep_check.py index 64de2a568f..71ce27db68 100644 --- a/pep_check.py +++ b/pep_check.py @@ -1,3 +1,12 @@ +""" +Checks all source files (.py) against PEP8 coding style. +The following rules are ignored: + - W293 - blank lines contain whitespace + - E501 - line too long (82 characters) + +Run this script before submitting a Pull-Request to check your code. +""" + import subprocess -subprocess.call(['pep8', '--exclude=migrations', '--ignore=EC04,W293,E501', 'InvenTree']) \ No newline at end of file +subprocess.call(['pep8', '--exclude=migrations', '--ignore=W293,E501', 'InvenTree']) From aa7ba9ddf322bbe5d79e4c2b1ffdfc7048208fd0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 1 Apr 2017 13:19:37 +1100 Subject: [PATCH 20/42] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index fdf28011fb..b8b975b804 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,6 @@ To create an initial user account, run the command `python InvenTree/manage.py c ## Documentation For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated) + +## Coding Style +All python code should conform to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. Run the *pep_check.py* script which will compare all source (.py) files against the PEP 8 style. From 9a9a039fc98a3dcf6e52a3b2a1c811eaec0484d9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 1 Apr 2017 13:31:48 +1100 Subject: [PATCH 21/42] Documentation for part fields --- InvenTree/part/models.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e9d246fd46..e9473d06dc 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -19,12 +19,27 @@ class PartCategory(InvenTreeTree): class Part(models.Model): """ Represents a """ + # Short name of the part name = models.CharField(max_length=100) + + # Longer description of the part (optional) description = models.CharField(max_length=250, blank=True) + + # Internal Part Number (optional) IPN = models.CharField(max_length=100, blank=True) + + # Part category - all parts must be assigned to a category category = models.ForeignKey(PartCategory, on_delete=models.CASCADE) - minimum_stock = models.IntegerField(default=0) + + # Minimum "allowed" stock level + minimum_stock = models.PositiveIntegerField(default=0) + + # Units of quantity for this part. Default is "pcs" units = models.CharField(max_length=20, default="pcs", blank=True) + + # Is this part "trackable"? + # Trackable parts can have unique instances which are assigned serial numbers + # and can have their movements tracked trackable = models.BooleanField(default=False) def __str__(self): @@ -54,7 +69,8 @@ class Part(models.Model): @property def projects(self): - """ Return a list of unique projects that this part is associated with + """ Return a list of unique projects that this part is associated with. + A part may be used in zero or more projects. """ project_ids = set() From 473bd441dfe6f377ec77e55d07c2c22038099c51 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 2 Apr 2017 23:06:43 +1000 Subject: [PATCH 22/42] Create roadmap.md --- roadmap.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 roadmap.md diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000000..b194aa17b2 --- /dev/null +++ b/roadmap.md @@ -0,0 +1,22 @@ +## InvenTree Roadmap + +### Design Goals + +InvenTree is intened to provide a stand-alone stock-management system that runs completely offline. + +It is designed to be run on a local server, and should not require the use of plugins/scripts that phone-home or load external content. + +(This ignores the use of bespoke plugins that may be implemented down the line, e.g. for OctoPart integration, etc) + +### 0.1 Release + +The goals for the initial release should be limited to the following: + +1. Fully implement a JSON API for the various apps and models +1. Design an initial front-end for querying data using this API + * Single-pase design is preferred, for the sake of responsiveness and intuitive interaction + * Investigate JS/AJAX engine - Angular? Bootstrap? +1. Allow users to view part category tree +1. Allow users to view all parts in a given category +1. "" edit parts +1. "" add new parts From bb680c34274e10b283489727102c19fd6b94c8e8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 2 Apr 2017 23:46:00 +1000 Subject: [PATCH 23/42] Update roadmap.md --- roadmap.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/roadmap.md b/roadmap.md index b194aa17b2..af9964f7b0 100644 --- a/roadmap.md +++ b/roadmap.md @@ -20,3 +20,9 @@ The goals for the initial release should be limited to the following: 1. Allow users to view all parts in a given category 1. "" edit parts 1. "" add new parts + +### TODO + +Research needed! + +django-restful in combination with angular seems the way to go. Extra tools provided via https://github.com/jrief/django-angular From 89e4cf899efcc1f0bc423ee6d01940b110515369 Mon Sep 17 00:00:00 2001 From: zakx Date: Mon, 10 Apr 2017 22:55:25 +0200 Subject: [PATCH 24/42] Modernized environment --- Makefile | 26 +++++++++++++++++++++++++ install.py | 44 ------------------------------------------ pep_check.py | 12 ------------ requirements/base.txt | 5 +++++ requirements/build.txt | 2 ++ requirements/dev.txt | 4 ++++ setup.cfg | 8 ++++++++ 7 files changed, 45 insertions(+), 56 deletions(-) create mode 100644 Makefile delete mode 100644 install.py delete mode 100644 pep_check.py create mode 100644 requirements/base.txt create mode 100644 requirements/build.txt create mode 100644 requirements/dev.txt create mode 100644 setup.cfg diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..d5d42d75cf --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +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 + +style: + flake8 + +test: + python InvenTree/manage.py test --noinput + +setup: + # TODO: replace this with a proper setup.py + pip install -U -r requirements/base.txt + python InvenTree/manage.py migrate --run-syncdb + python InvenTree/manage.py check + +develop: + pip install -U -r requirements/dev.txt + +superuser: + python InvenTree/manage.py createsuperuser diff --git a/install.py b/install.py deleted file mode 100644 index 090fdd3e79..0000000000 --- a/install.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import print_function - -import subprocess -import argparse - -def manage(*arg): - args = ["python", "InvenTree/manage.py"] - - for a in arg: - args.append(a) - - subprocess.call(args) - -parser = argparse.ArgumentParser(description="Install InvenTree inventory management system") - -parser.add_argument('-u', '--update', help='Update only, do not try to install required components', action='store_true') - -args = parser.parse_args() - -# If 'update' is specified, don't perform initial installation -if not args.update: - # Install django requirements - subprocess.call(["pip", "install", "django", "-q"]) - subprocess.call(["pip", "install", "djangorestframework", "-q"]) - - # Initial database setup - manage("migrate") - -# Make migrations for all apps -manage("makemigrations", "part") -manage("makemigrations", "stock") -manage("makemigrations", "supplier") -manage("makemigrations", "project") -manage("makemigrations", "track") - -# Update the database -manage("migrate") - -# Check for errors -manage("check") - -if not args.update: - print("\n\nAdmin account:\nIf a superuser is not already installed,") - print("run the command 'python InvenTree/manage.py createsuperuser'") diff --git a/pep_check.py b/pep_check.py deleted file mode 100644 index 71ce27db68..0000000000 --- a/pep_check.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Checks all source files (.py) against PEP8 coding style. -The following rules are ignored: - - W293 - blank lines contain whitespace - - E501 - line too long (82 characters) - -Run this script before submitting a Pull-Request to check your code. -""" - -import subprocess - -subprocess.call(['pep8', '--exclude=migrations', '--ignore=W293,E501', 'InvenTree']) diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000000..0c3901f32b --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,5 @@ +Django==1.11 +djangorestframework==3.6.2 +pep8==1.7.0 +pkg-resources==0.0.0 +pytz==2017.2 diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 0000000000..c400442ceb --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,2 @@ +-r base.txt +flake8==3.3.0 diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000000..3ec7263b93 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,4 @@ +-r build.txt +django-extensions==1.7.8 +graphviz==0.6 +ipython==5.3.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..ea418b2daf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[flake8] +ignore = + # - W293 - blank lines contain whitespace + W293, + # - E501 - line too long (82 characters) + E501 +exclude = .git,__pycache__ +max-complexity = 10 From 82541b9b861ae5e00006ea502609c82cb6409e39 Mon Sep 17 00:00:00 2001 From: zakx Date: Mon, 10 Apr 2017 22:55:55 +0200 Subject: [PATCH 25/42] Updating travis file to use Makefile commands --- .travis.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3f55eeb22b..42c419baae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,8 @@ python: - 3.4 before_install: - - pip install pep8 - - pip install django - - pip install djangorestframework - + - make setup + script: - - python pep_check.py - - python InvenTree/manage.py check - - python InvenTree/manage.py test --noinput \ No newline at end of file + - make style + - make test From 428015ecf5e7bd09ea65b3f857bc91a5f0ac0d88 Mon Sep 17 00:00:00 2001 From: zakx Date: Mon, 10 Apr 2017 22:56:09 +0200 Subject: [PATCH 26/42] Adding reminder to remove pre-generated SECRET_KEY Later on, we'll need environment-based default configs anyway, so this will probably be done together. --- InvenTree/InvenTree/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 8585f5b911..ab2205a296 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -20,6 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! +# TODO: remove this SECRET_KEY = 'oc2z%5)lu#jsxi#wpg)700z@v48)2aa_yn(a(3qg!z!fw&tr9f' # SECURITY WARNING: don't run with debug turned on in production! @@ -40,7 +41,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - + # InvenTree apps 'part.apps.PartConfig', 'project.apps.ProjectConfig', From d27c68e4eecf5d76ff2f07eb284983fc12189d84 Mon Sep 17 00:00:00 2001 From: zakx Date: Mon, 10 Apr 2017 22:57:13 +0200 Subject: [PATCH 27/42] Updating README to reflect new tooling --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b8b975b804..18c67aebbf 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,22 @@ Open Source Inventory Management System [![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) ## Installation -When first installing InvenTree, initial database configuration must be performed. This is handled by the `install.py` script, which performs the following actions: +It is recommended to set up a clean Python 3.4+ virtual environment first: +`mkdir ~/.env && python3 -m venv ~/.env/InvenTree && source ~/.env/InvenTree/bin/activate` -1. Installs required django packages (requires [pip](https://pypi.python.org/pypi/pip)) +You can then continue running `make setup` (which will be replaced by a proper setup.py soon). This will do the following: + +1. Installs required Python dependencies (requires [pip](https://pypi.python.org/pypi/pip), should be part of your virtual environment by default) 1. Performs initial database setup 1. Updates database tables for all InvenTree components -This script can also be used to update the installation if changes have been made to the database configuration. +This command can also be used to update the installation if changes have been made to the database configuration. -To create an initial user account, run the command `python InvenTree/manage.py createsuperuser` +To create an initial user account, run the command `make superuser`. ## Documentation For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated) ## Coding Style -All python code should conform to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. Run the *pep_check.py* script which will compare all source (.py) files against the PEP 8 style. +If you'd like to contribute, install our development dependencies using `make develop`. +All Python code should conform to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. Run `make style` which will compare all source (.py) files against the PEP 8 style. Tests can be run using `make test`. From c5b2206f94c0d9b04177fc9305c49b84686deecb Mon Sep 17 00:00:00 2001 From: zakx Date: Mon, 10 Apr 2017 23:03:12 +0200 Subject: [PATCH 28/42] Removing bogus dependencies --- requirements/base.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 0c3901f32b..1a4283d80d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,2 @@ Django==1.11 djangorestframework==3.6.2 -pep8==1.7.0 -pkg-resources==0.0.0 -pytz==2017.2 From 38725e6ad044cfc8bbfdd6e6498a4a2d8983cfdb Mon Sep 17 00:00:00 2001 From: zakx Date: Mon, 10 Apr 2017 23:15:11 +0200 Subject: [PATCH 29/42] Making sure travis has the build.txt requirements --- .travis.yml | 1 + Makefile | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 42c419baae..9f63351f98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: before_install: - make setup + - make setup_ci script: - make style diff --git a/Makefile b/Makefile index d5d42d75cf..307922603a 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,9 @@ setup: python InvenTree/manage.py migrate --run-syncdb python InvenTree/manage.py check +setup_ci: + pip install -U -r requirements/build.txt + develop: pip install -U -r requirements/dev.txt From e06121ebdac6063c8d0608b694ba11b7e136330b Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 09:41:03 +1000 Subject: [PATCH 30/42] Updated API URLs --- InvenTree/InvenTree/api_urls.py | 12 ++++++ InvenTree/InvenTree/models.py | 69 ++++++++++++++++++--------------- InvenTree/InvenTree/urls.py | 17 +------- InvenTree/part/api_urls.py | 17 ++++++++ InvenTree/part/models.py | 6 +++ InvenTree/part/serializers.py | 48 +++++++++++++++++++++-- InvenTree/part/urls.py | 16 +++----- InvenTree/part/views.py | 24 +++++++----- InvenTree/project/api_urls.py | 6 +++ 9 files changed, 144 insertions(+), 71 deletions(-) create mode 100644 InvenTree/InvenTree/api_urls.py create mode 100644 InvenTree/part/api_urls.py create mode 100644 InvenTree/project/api_urls.py diff --git a/InvenTree/InvenTree/api_urls.py b/InvenTree/InvenTree/api_urls.py new file mode 100644 index 0000000000..0a742f2fe5 --- /dev/null +++ b/InvenTree/InvenTree/api_urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url, include +from django.contrib import admin + +admin.site.site_header = "InvenTree Admin" + +urlpatterns = [ + url(r'^stock/', include('stock.urls')), + url(r'^part/', include('part.api_urls')), + url(r'^supplier/', include('supplier.urls')), + url(r'^track/', include('track.urls')), + url(r'^project/', include('project.api_urls')) +] diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 7e06b1a183..0de4d636bb 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -8,10 +8,10 @@ from django.contrib.contenttypes.models import ContentType class Company(models.Model): """ Abstract model representing an external company """ - + class Meta: abstract = True - + name = models.CharField(max_length=100) URL = models.URLField(blank=True) address = models.CharField(max_length=200, @@ -33,10 +33,10 @@ class InvenTreeTree(models.Model): - Each Category has one parent Category, which can be blank (for a top-level Category). - Each Category can have zero-or-more child Categor(y/ies) """ - + class Meta: abstract = True - + name = models.CharField(max_length=100) description = models.CharField(max_length=250, blank=True) parent = models.ForeignKey('self', @@ -44,95 +44,102 @@ class InvenTreeTree(models.Model): blank=True, null=True, related_name='children') - + def getUniqueParents(self, unique=None): """ Return a flat set of all parent items that exist above this node. If any parents are repeated (which would be very bad!), the process is halted """ - + if unique is None: unique = set() else: unique.add(self.id) - + if self.parent and self.parent.id not in unique: self.parent.getUniqueParents(unique) - + return unique - + def getUniqueChildren(self, unique=None): """ Return a flat set of all child items that exist under this node. If any child items are repeated, the repetitions are omitted. """ - + if unique is None: unique = set() - + if self.id in unique: return unique - + unique.add(self.id) - + # Some magic to get around the limitations of abstract models contents = ContentType.objects.get_for_model(type(self)) children = contents.get_all_objects_for_this_type(parent=self.id) - + return unique - + + @property + def children(self): + contents = ContentType.objects.get_for_model(type(self)) + children = contents.get_all_objects_for_this_type(parent=self.id) + + return children + def getAcceptableParents(self): """ Returns a list of acceptable parent items within this model Acceptable parents are ones which are not underneath this item. Setting the parent of an item to its own child results in recursion. """ contents = ContentType.objects.get_for_model(type(self)) - + available = contents.get_all_objects_for_this_type() - + # List of child IDs childs = getUniqueChildren() - + acceptable = [None] - + for a in available: if a.id not in childs: acceptable.append(a) - + return acceptable - + @property def parentpath(self): """ Return the parent path of this category - + Todo: This function is recursive and expensive. It should be reworked such that only a single db call is required """ - + if self.parent: return self.parent.parentpath + [self.parent] else: return [] - + @property def path(self): if self.parent: return "/".join([p.name for p in self.parentpath]) + "/" + self.name else: return self.name - + def __setattr__(self, attrname, val): """ Custom Attribute Setting function - + Parent: Setting the parent of an item to its own child results in an infinite loop. The parent of an item cannot be set to: a) Its own ID b) The ID of any child items that exist underneath it - + Name: Tree node names are limited to a reduced character set """ - + if attrname == 'parent_id': # If current ID is None, continue # - This object is just being created @@ -153,14 +160,14 @@ class InvenTreeTree(models.Model): # Prohibit certain characters from tree node names elif attrname == 'name': val = val.translate({ord(c): None for c in "!@#$%^&*'\"\\/[]{}<>,|+=~`"}) - + super(InvenTreeTree, self).__setattr__(attrname, val) def __str__(self): """ String representation of a category is the full path to that category - + Todo: This is recursive - Make it not so. """ - + return self.path diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 52b69c4a95..b00372d97b 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -1,25 +1,10 @@ -"""InvenTree URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.10/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" - from django.conf.urls import url, include from django.contrib import admin admin.site.site_header = "InvenTree Admin" urlpatterns = [ + url(r'^api/', include('InvenTree.api_urls')), url(r'^stock/', include('stock.urls')), url(r'^part/', include('part.urls')), url(r'^supplier/', include('supplier.urls')), diff --git a/InvenTree/part/api_urls.py b/InvenTree/part/api_urls.py new file mode 100644 index 0000000000..c220617154 --- /dev/null +++ b/InvenTree/part/api_urls.py @@ -0,0 +1,17 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + # Display part detail + url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), + + # Display a single part category + url(r'^category/(?P[0-9]+)/$', views.PartCategoryDetail.as_view()), + + # Display a list of top-level categories + url(r'^category/$', views.PartCategoryList.as_view()), + + # Display list of all parts + url(r'^$', views.PartList.as_view()) +] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e9473d06dc..9ba26149ae 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -15,6 +15,12 @@ class PartCategory(InvenTreeTree): verbose_name = "Part Category" verbose_name_plural = "Part Categories" + @property + def parts(self): + parts_list = self.part_set.all() + print(parts_list) + return parts_list + class Part(models.Model): """ Represents a """ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 9dea0f0231..523b55f9eb 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -4,14 +4,21 @@ from .models import Part, PartCategory, PartParameter class ParameterSerializer(serializers.ModelSerializer): + """ Serializer for a PartParameter + """ + class Meta: model = PartParameter - fields = ('name', + fields = ('pk', + 'name', 'value', 'units') -class PartSerializer(serializers.ModelSerializer): +class PartDetailSerializer(serializers.ModelSerializer): + """ Serializer for complete detail information of a part. + Used when displaying all details of a single component. + """ params = ParameterSerializer(source='parameters', many=True) @@ -26,11 +33,44 @@ class PartSerializer(serializers.ModelSerializer): 'params') -class PartCategorySerializer(serializers.ModelSerializer): +class PartBriefSerializer(serializers.ModelSerializer): + """ Serializer for displaying overview of a part. + Used e.g. for displaying list of parts in a category. + """ + + class Meta: + model = Part + fields = ('pk', + 'name', + 'IPN', + 'description', + 'category', + 'stock') + + +class PartCategoryBriefSerializer(serializers.ModelSerializer): + + class Meta: + model = PartCategory + fields = ('pk', + 'name', + 'description') + + +class PartCategoryDetailSerializer(serializers.ModelSerializer): + + # List of parts in this category + parts = PartBriefSerializer(many=True) + + # List of child categories under this one + children = PartCategoryBriefSerializer(many=True) + class Meta: model = PartCategory fields = ('pk', 'name', 'description', 'parent', - 'path') + 'path', + 'children', + 'parts') diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 01ebb11cbf..f14151b51a 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -3,15 +3,9 @@ from django.conf.urls import url from . import views urlpatterns = [ - # Display part detail - url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), - - # Display a single part category - url(r'^category/(?P[0-9]+)/$', views.PartCategoryDetail.as_view()), - - # Display a list of top-level categories - url(r'^category/$', views.PartCategoryList.as_view()), - - # Display list of parts - url(r'^$', views.PartList.as_view()) + # part landing page + url(r'^$', views.part_index), + + # part category landing page + url(r'^category/$', views.category_index) ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 28d447303e..37b59486ca 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -4,32 +4,38 @@ from django.http import HttpResponse, Http404 from rest_framework import generics from .models import PartCategory, Part -from .serializers import PartSerializer, PartCategorySerializer +from .serializers import PartBriefSerializer, PartDetailSerializer +from .serializers import PartCategoryBriefSerializer, PartCategoryDetailSerializer -def index(request): +def part_index(request): return HttpResponse("Hello world. This is the parts page") +def category_index(request): + return HttpResponse("This is the category page") class PartDetail(generics.RetrieveAPIView): queryset = Part.objects.all() - serializer_class = PartSerializer + serializer_class = PartDetailSerializer class PartList(generics.ListAPIView): queryset = Part.objects.all() - serializer_class = PartSerializer + serializer_class = PartBriefSerializer class PartCategoryDetail(generics.RetrieveAPIView): - + """ Return information on a single PartCategory + """ queryset = PartCategory.objects.all() - serializer_class = PartCategorySerializer + serializer_class = PartCategoryDetailSerializer class PartCategoryList(generics.ListAPIView): - - queryset = PartCategory.objects.all() - serializer_class = PartCategorySerializer + """ Return a list of all top-level part categories. + Categories are considered "top-level" if they do not have a parent + """ + queryset = PartCategory.objects.filter(parent=None) + serializer_class = PartCategoryBriefSerializer diff --git a/InvenTree/project/api_urls.py b/InvenTree/project/api_urls.py new file mode 100644 index 0000000000..65b34d128c --- /dev/null +++ b/InvenTree/project/api_urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ +] From 1cddec10357ac5e5458850f169412ffaf01b1ce5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 09:58:45 +1000 Subject: [PATCH 31/42] - Fixes for getUniqueChildren - Flattened API structure --- InvenTree/InvenTree/api_urls.py | 12 ------------ InvenTree/InvenTree/models.py | 3 +++ InvenTree/InvenTree/urls.py | 1 - InvenTree/part/api_urls.py | 17 ----------------- InvenTree/part/urls.py | 14 ++++++++++---- InvenTree/project/api_urls.py | 6 ------ 6 files changed, 13 insertions(+), 40 deletions(-) delete mode 100644 InvenTree/InvenTree/api_urls.py delete mode 100644 InvenTree/part/api_urls.py delete mode 100644 InvenTree/project/api_urls.py diff --git a/InvenTree/InvenTree/api_urls.py b/InvenTree/InvenTree/api_urls.py deleted file mode 100644 index 0a742f2fe5..0000000000 --- a/InvenTree/InvenTree/api_urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.conf.urls import url, include -from django.contrib import admin - -admin.site.site_header = "InvenTree Admin" - -urlpatterns = [ - url(r'^stock/', include('stock.urls')), - url(r'^part/', include('part.api_urls')), - url(r'^supplier/', include('supplier.urls')), - url(r'^track/', include('track.urls')), - url(r'^project/', include('project.api_urls')) -] diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 0de4d636bb..033a8b3a09 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -77,6 +77,9 @@ class InvenTreeTree(models.Model): contents = ContentType.objects.get_for_model(type(self)) children = contents.get_all_objects_for_this_type(parent=self.id) + for child in children: + child.getUniqueChildren(unique) + return unique @property diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b00372d97b..f4a6b74667 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -4,7 +4,6 @@ from django.contrib import admin admin.site.site_header = "InvenTree Admin" urlpatterns = [ - url(r'^api/', include('InvenTree.api_urls')), url(r'^stock/', include('stock.urls')), url(r'^part/', include('part.urls')), url(r'^supplier/', include('supplier.urls')), diff --git a/InvenTree/part/api_urls.py b/InvenTree/part/api_urls.py deleted file mode 100644 index c220617154..0000000000 --- a/InvenTree/part/api_urls.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.conf.urls import url - -from . import views - -urlpatterns = [ - # Display part detail - url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), - - # Display a single part category - url(r'^category/(?P[0-9]+)/$', views.PartCategoryDetail.as_view()), - - # Display a list of top-level categories - url(r'^category/$', views.PartCategoryList.as_view()), - - # Display list of all parts - url(r'^$', views.PartList.as_view()) -] diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index f14151b51a..c220617154 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -3,9 +3,15 @@ from django.conf.urls import url from . import views urlpatterns = [ - # part landing page - url(r'^$', views.part_index), + # Display part detail + url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), - # part category landing page - url(r'^category/$', views.category_index) + # Display a single part category + url(r'^category/(?P[0-9]+)/$', views.PartCategoryDetail.as_view()), + + # Display a list of top-level categories + url(r'^category/$', views.PartCategoryList.as_view()), + + # Display list of all parts + url(r'^$', views.PartList.as_view()) ] diff --git a/InvenTree/project/api_urls.py b/InvenTree/project/api_urls.py deleted file mode 100644 index 65b34d128c..0000000000 --- a/InvenTree/project/api_urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.conf.urls import url - -from . import views - -urlpatterns = [ -] From 338dcd3b72d59a1e2dbe516370389c89b6e23820 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 17:21:05 +1000 Subject: [PATCH 32/42] Fixing python errors --- InvenTree/InvenTree/models.py | 3 +-- InvenTree/manage.py | 2 +- InvenTree/part/admin.py | 8 ++++---- InvenTree/part/models.py | 3 +-- InvenTree/part/tests.py | 2 +- InvenTree/part/views.py | 6 +++--- InvenTree/project/admin.py | 1 + InvenTree/project/tests.py | 2 +- InvenTree/project/views.py | 1 - InvenTree/stock/admin.py | 1 + InvenTree/stock/tests.py | 2 +- InvenTree/stock/views.py | 9 ++++----- InvenTree/supplier/admin.py | 1 + InvenTree/supplier/models.py | 2 +- InvenTree/supplier/tests.py | 2 +- InvenTree/track/admin.py | 1 + InvenTree/track/tests.py | 2 +- InvenTree/track/views.py | 1 - Makefile | 4 ++++ 19 files changed, 28 insertions(+), 25 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 033a8b3a09..4eb8dd8daa 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from django.db import models -from django.core.exceptions import ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType @@ -99,7 +98,7 @@ class InvenTreeTree(models.Model): available = contents.get_all_objects_for_this_type() # List of child IDs - childs = getUniqueChildren() + childs = self.getUniqueChildren() acceptable = [None] diff --git a/InvenTree/manage.py b/InvenTree/manage.py index cfaeab5f9e..dc8d382fac 100755 --- a/InvenTree/manage.py +++ b/InvenTree/manage.py @@ -11,7 +11,7 @@ if __name__ == "__main__": # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django + import django # NOQA except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index f36f4f8931..dd5ec73763 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -4,14 +4,14 @@ from .models import PartCategory, Part, PartParameter, PartParameterTemplate, Ca class PartAdmin(admin.ModelAdmin): - + list_display = ('name', 'IPN', 'stock', 'category') class PartCategoryAdmin(admin.ModelAdmin): - + list_display = ('name', 'path', 'description') - + class ParameterTemplateAdmin(admin.ModelAdmin): list_display = ('name', 'units', 'format') @@ -20,7 +20,7 @@ class ParameterTemplateAdmin(admin.ModelAdmin): class ParameterAdmin(admin.ModelAdmin): list_display = ('part', 'template', 'value') - + admin.site.register(Part, PartAdmin) admin.site.register(PartCategory, PartCategoryAdmin) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 9ba26149ae..740bcd0cfd 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from django.utils.translation import ugettext as _ from django.db import models from django.db.models import Sum -from django.core.exceptions import ObjectDoesNotExist, ValidationError from InvenTree.models import InvenTreeTree @@ -116,7 +115,7 @@ class PartParameterTemplate(models.Model): PARAM_BOOL: _("Bool") } - format = models.IntegerField( + format = models.PositiveIntegerField( default=PARAM_NUMERIC, choices=PARAM_TYPE_CODES.items()) diff --git a/InvenTree/part/tests.py b/InvenTree/part/tests.py index 7ce503c2dd..a79ca8be56 100644 --- a/InvenTree/part/tests.py +++ b/InvenTree/part/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 37b59486ca..cc60db27b7 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,6 +1,4 @@ -from django.shortcuts import render, get_object_or_404 -from django.http import HttpResponse, Http404 - +from django.http import HttpResponse from rest_framework import generics from .models import PartCategory, Part @@ -11,9 +9,11 @@ from .serializers import PartCategoryBriefSerializer, PartCategoryDetailSerializ def part_index(request): return HttpResponse("Hello world. This is the parts page") + def category_index(request): return HttpResponse("This is the category page") + class PartDetail(generics.RetrieveAPIView): queryset = Part.objects.all() diff --git a/InvenTree/project/admin.py b/InvenTree/project/admin.py index 0f1ad27b11..2d8b43b0d9 100644 --- a/InvenTree/project/admin.py +++ b/InvenTree/project/admin.py @@ -18,6 +18,7 @@ class ProjectPartAdmin(admin.ModelAdmin): class ProjectRunAdmin(admin.ModelAdmin): list_display = ('project', 'quantity', 'run_date') + admin.site.register(ProjectCategory, ProjectCategoryAdmin) admin.site.register(Project, ProjectAdmin) admin.site.register(ProjectPart, ProjectPartAdmin) diff --git a/InvenTree/project/tests.py b/InvenTree/project/tests.py index 7ce503c2dd..a79ca8be56 100644 --- a/InvenTree/project/tests.py +++ b/InvenTree/project/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index a9b40c0cc5..adbd6ac6ef 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -1,4 +1,3 @@ -from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 94de179851..7bfd55fe3c 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -10,5 +10,6 @@ class WarehouseAdmin(admin.ModelAdmin): class StockItemAdmin(admin.ModelAdmin): list_display = ('part', 'quantity', 'location', 'status', 'updated') + admin.site.register(Warehouse, WarehouseAdmin) admin.site.register(StockItem, StockItemAdmin) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 7ce503c2dd..a79ca8be56 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 394fe97ad0..3fbebb9bd0 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1,11 +1,10 @@ -from django.shortcuts import render, get_object_or_404 -from django.http import HttpResponse +from django.shortcuts import render -from .models import Warehouse, StockItem +from .models import Warehouse def index(request): - + warehouses = Warehouse.objects.filter(parent=None) - + return render(request, 'stock/index.html', {'warehouses': warehouses}) diff --git a/InvenTree/supplier/admin.py b/InvenTree/supplier/admin.py index 9e9d53dfa4..cf19edd2b0 100644 --- a/InvenTree/supplier/admin.py +++ b/InvenTree/supplier/admin.py @@ -6,6 +6,7 @@ from .models import Supplier, SupplierPart, Customer, Manufacturer class CompanyAdmin(admin.ModelAdmin): list_display = ('name', 'URL', 'contact') + admin.site.register(Customer, CompanyAdmin) admin.site.register(Supplier, CompanyAdmin) admin.site.register(Manufacturer, CompanyAdmin) diff --git a/InvenTree/supplier/models.py b/InvenTree/supplier/models.py index bbd0f2df3f..bd5d20dc86 100644 --- a/InvenTree/supplier/models.py +++ b/InvenTree/supplier/models.py @@ -77,7 +77,7 @@ class SupplierPriceBreak(models.Model): def __str__(self): return "{mpn} - {cost}{currency} @ {quan}".format( - mpn=part.MPN, + mpn=self.part.MPN, cost=self.cost, currency=self.currency if self.currency else '', quan=self.quantity) diff --git a/InvenTree/supplier/tests.py b/InvenTree/supplier/tests.py index 7ce503c2dd..a79ca8be56 100644 --- a/InvenTree/supplier/tests.py +++ b/InvenTree/supplier/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/InvenTree/track/admin.py b/InvenTree/track/admin.py index 17a3aa6cb2..573e458878 100644 --- a/InvenTree/track/admin.py +++ b/InvenTree/track/admin.py @@ -6,4 +6,5 @@ from .models import UniquePart class UniquePartAdmin(admin.ModelAdmin): list_display = ('part', 'revision', 'serial', 'status', 'creation_date') + admin.site.register(UniquePart, UniquePartAdmin) diff --git a/InvenTree/track/tests.py b/InvenTree/track/tests.py index 7ce503c2dd..a79ca8be56 100644 --- a/InvenTree/track/tests.py +++ b/InvenTree/track/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/InvenTree/track/views.py b/InvenTree/track/views.py index 5986b94af1..0a26c7bf12 100644 --- a/InvenTree/track/views.py +++ b/InvenTree/track/views.py @@ -1,4 +1,3 @@ -from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse diff --git a/Makefile b/Makefile index 307922603a..18b1a97499 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,10 @@ test: setup: # TODO: replace this with a proper setup.py pip install -U -r requirements/base.txt + migrate + +migrate: + python InvenTree/manage.py makemigrations python InvenTree/manage.py migrate --run-syncdb python InvenTree/manage.py check From 599cdc0a7bbb8f153acb4aed105e971d01fe84e8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 17:27:14 +1000 Subject: [PATCH 33/42] fixes for makefile --- Makefile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 18b1a97499..8cc591476e 100644 --- a/Makefile +++ b/Makefile @@ -13,16 +13,17 @@ style: test: python InvenTree/manage.py test --noinput -setup: - # TODO: replace this with a proper setup.py - pip install -U -r requirements/base.txt - migrate - migrate: python InvenTree/manage.py makemigrations python InvenTree/manage.py migrate --run-syncdb python InvenTree/manage.py check +install: + # TODO: replace this with a proper setup.py + pip install -U -r requirements/base.txt + +setup: install migrate + setup_ci: pip install -U -r requirements/build.txt From 293bf41048ba85b0a56e57f644fb49baefa69554 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 17:36:51 +1000 Subject: [PATCH 34/42] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 18c67aebbf..016119fbe7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# InvenTree -Open Source Inventory Management System - [![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) +# InvenTree +InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications. + ## Installation It is recommended to set up a clean Python 3.4+ virtual environment first: `mkdir ~/.env && python3 -m venv ~/.env/InvenTree && source ~/.env/InvenTree/bin/activate` From 15582369d248f31cc9cd0fb3e7ad7aafbd3cc3d8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 18:58:44 +1000 Subject: [PATCH 35/42] API for stock app --- InvenTree/part/models.py | 1 - InvenTree/stock/admin.py | 6 ++-- InvenTree/stock/models.py | 12 ++++++-- InvenTree/stock/serializers.py | 52 ++++++++++++++++++++++++++++++++++ InvenTree/stock/urls.py | 9 +++++- InvenTree/stock/views.py | 33 +++++++++++++++++---- 6 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 InvenTree/stock/serializers.py diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 740bcd0cfd..b664688bd8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -17,7 +17,6 @@ class PartCategory(InvenTreeTree): @property def parts(self): parts_list = self.part_set.all() - print(parts_list) return parts_list diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 7bfd55fe3c..6ce4df95c5 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -1,9 +1,9 @@ from django.contrib import admin -from .models import Warehouse, StockItem +from .models import StockLocation, StockItem -class WarehouseAdmin(admin.ModelAdmin): +class LocationAdmin(admin.ModelAdmin): list_display = ('name', 'path', 'description') @@ -11,5 +11,5 @@ class StockItemAdmin(admin.ModelAdmin): list_display = ('part', 'quantity', 'location', 'status', 'updated') -admin.site.register(Warehouse, WarehouseAdmin) +admin.site.register(StockLocation, LocationAdmin) admin.site.register(StockItem, StockItemAdmin) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 40d95a5676..dc5f412420 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -6,15 +6,21 @@ from part.models import Part from InvenTree.models import InvenTreeTree -class Warehouse(InvenTreeTree): - pass +class StockLocation(InvenTreeTree): + """ Organization tree for StockItem objects + """ + + @property + def items(self): + stock_list = self.stockitem_set.all() + return stock_list class StockItem(models.Model): part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='locations') - location = models.ForeignKey(Warehouse, on_delete=models.CASCADE) + location = models.ForeignKey(StockLocation, on_delete=models.CASCADE) quantity = models.PositiveIntegerField() updated = models.DateField(auto_now=True) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py new file mode 100644 index 0000000000..bf07dfa1de --- /dev/null +++ b/InvenTree/stock/serializers.py @@ -0,0 +1,52 @@ +from rest_framework import serializers + +from .models import StockItem, StockLocation + + +class StockItemSerializer(serializers.ModelSerializer): + """ Serializer for a StockItem + """ + + class Meta: + model = StockItem + fields = ('pk', + 'part', + 'location', + 'quantity', + 'status', + 'updated', + 'last_checked', + 'review_needed', + 'expected_arrival') + + +class LocationBriefSerializer(serializers.ModelSerializer): + """ Brief information about a stock location + """ + + class Meta: + model = StockLocation + fields = ('pk', + 'name', + 'description') + + +class LocationDetailSerializer(serializers.ModelSerializer): + """ Detailed information about a stock location + """ + + # List of all stock items in this location + items = StockItemSerializer(many=True) + + # List of all child locations under this one + children = LocationBriefSerializer(many=True) + + class Meta: + model = StockLocation + fields = ('pk', + 'name', + 'description', + 'parent', + 'path', + 'children', + 'items') diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 9cb3403af4..a9dc1bc09e 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -3,5 +3,12 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^$', views.index, name='index') + # List all stock quantities for a given part + url(r'^part/(?P[0-9]+)$', views.PartStockDetail.as_view()), + + # List all stock items in a given location + url(r'^location/(?P[0-9]+)$', views.LocationDetail.as_view()), + + # List all top-level locations + url(r'^location/$', views.LocationList.as_view()) ] diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 3fbebb9bd0..5f76cbebf1 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1,10 +1,33 @@ -from django.shortcuts import render +from rest_framework import generics -from .models import Warehouse +from .models import StockLocation, StockItem + +from .serializers import StockItemSerializer, LocationDetailSerializer -def index(request): +class PartStockDetail(generics.ListAPIView): + """ Return a list of all stockitems for a given part + """ - warehouses = Warehouse.objects.filter(parent=None) + serializer_class = StockItemSerializer - return render(request, 'stock/index.html', {'warehouses': warehouses}) + def get_queryset(self): + part_id = self.kwargs['part'] + return StockItem.objects.filter(part=part_id) + + +class LocationDetail(generics.RetrieveAPIView): + """ Return information on a specific stock location + """ + + queryset = StockLocation.objects.all() + serializer_class = LocationDetailSerializer + + +class LocationList(generics.ListAPIView): + """ Return a list of top-level locations + Locations are considered "top-level" if they do not have a parent + """ + + queryset = StockLocation.objects.filter(parent=None) + serializer_class = LocationDetailSerializer From 3704ad34dc0332791624bfabb265b63b461402eb Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 23:07:02 +1000 Subject: [PATCH 36/42] Updated part API --- InvenTree/part/models.py | 3 +-- InvenTree/part/urls.py | 8 ++++---- InvenTree/part/views.py | 9 --------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b664688bd8..c479037743 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -16,8 +16,7 @@ class PartCategory(InvenTreeTree): @property def parts(self): - parts_list = self.part_set.all() - return parts_list + return self.part_set.all() class Part(models.Model): diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index c220617154..601e2d74ee 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -3,15 +3,15 @@ from django.conf.urls import url from . import views urlpatterns = [ - # Display part detail + # Single part detail url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), - # Display a single part category + # Part category detail url(r'^category/(?P[0-9]+)/$', views.PartCategoryDetail.as_view()), - # Display a list of top-level categories + # List of top-level categories url(r'^category/$', views.PartCategoryList.as_view()), - # Display list of all parts + # List of all parts url(r'^$', views.PartList.as_view()) ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index cc60db27b7..85451ef00e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,4 +1,3 @@ -from django.http import HttpResponse from rest_framework import generics from .models import PartCategory, Part @@ -6,14 +5,6 @@ from .serializers import PartBriefSerializer, PartDetailSerializer from .serializers import PartCategoryBriefSerializer, PartCategoryDetailSerializer -def part_index(request): - return HttpResponse("Hello world. This is the parts page") - - -def category_index(request): - return HttpResponse("This is the category page") - - class PartDetail(generics.RetrieveAPIView): queryset = Part.objects.all() From 0ca8a751665532fc0fa0bcbf9b22b28e60350c5e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 23:12:07 +1000 Subject: [PATCH 37/42] Added projects API --- InvenTree/project/models.py | 6 ++- InvenTree/project/serializers.py | 66 ++++++++++++++++++++++++++++++++ InvenTree/project/urls.py | 15 +++++++- InvenTree/project/views.py | 40 +++++++++++++++++-- 4 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 InvenTree/project/serializers.py diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index 15f87ae006..c289f5ebd3 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -17,6 +17,10 @@ class ProjectCategory(InvenTreeTree): verbose_name = "Project Category" verbose_name_plural = "Project Categories" + @property + def projects(self): + return self.project_set.all() + class Project(models.Model): """ A Project takes multiple Part objects. @@ -57,7 +61,7 @@ class ProjectPart(models.Model): quantity = models.PositiveIntegerField(default=1) overage = models.FloatField(default=0) overage_type = models.PositiveIntegerField( - default=1, + default=OVERAGE_ABSOLUTE, choices=OVARAGE_CODES.items()) def __str__(self): diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py new file mode 100644 index 0000000000..4123968042 --- /dev/null +++ b/InvenTree/project/serializers.py @@ -0,0 +1,66 @@ +from rest_framework import serializers + +from .models import ProjectCategory, Project, ProjectPart + +from part.serializers import PartBriefSerializer + + +class ProjectPartSerializer(serializers.ModelSerializer): + + class Meta: + model = ProjectPart + fields = ('pk', + 'part', + 'project', + 'quantity', + 'overage', + 'overage_type') + + +class ProjectBriefSerializer(serializers.ModelSerializer): + """ Serializer for displaying brief overview of a project + """ + + + class Meta: + model = Project + fields = ('pk', + 'name', + 'description', + 'category') + + +class ProjectDetailSerializer(serializers.ModelSerializer): + """ Serializer for detailed project information + """ + + class Meta: + model = Project + fields = ('pk', + 'name', + 'description', + 'category') + + +class ProjectCategoryBriefSerializer(serializers.ModelSerializer): + + class Meta: + model = ProjectCategory + fields = ('pk','name','description') + + +class ProjectCategoryDetailSerializer(serializers.ModelSerializer): + + projects = ProjectBriefSerializer(many=True) + + children = ProjectCategoryBriefSerializer(many=True) + + class Meta: + model = ProjectCategory + fields = ('pk', + 'name', + 'description', + 'parent', + 'path', + 'children', + 'projects') diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py index 9cb3403af4..2758a64cc8 100644 --- a/InvenTree/project/urls.py +++ b/InvenTree/project/urls.py @@ -3,5 +3,18 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^$', views.index, name='index') + # Single project detail + url(r'^(?P[0-9]+)/$', views.ProjectDetail.as_view()), + + # Parts associated with a project + url(r'^(?P[0-9]+)/parts$', views.ProjectPartsList.as_view()), + + # List of all projects + url(r'^$', views.ProjectList.as_view()), + + # List of top-level project categories + url(r'^category/$', views.ProjectCategoryList.as_view()), + + # Detail of a single project category + url(r'^category/(?P[0-9]+)/$', views.ProjectCategoryDetail.as_view()) ] diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index adbd6ac6ef..a92158ff48 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -1,5 +1,39 @@ -from django.http import HttpResponse +from rest_framework import generics + +from .models import ProjectCategory, Project, ProjectPart +from .serializers import ProjectBriefSerializer, ProjectDetailSerializer +from .serializers import ProjectCategoryBriefSerializer, ProjectCategoryDetailSerializer +from .serializers import ProjectPartSerializer + +class ProjectDetail(generics.RetrieveAPIView): + + queryset = Project.objects.all() + serializer_class = ProjectDetailSerializer -def index(request): - return HttpResponse("This is the Projects page") +class ProjectList(generics.ListAPIView): + + queryset = Project.objects.all() + serializer_class = ProjectBriefSerializer + + + +class ProjectCategoryDetail(generics.RetrieveAPIView): + + queryset = ProjectCategory.objects.all() + serializer_class = ProjectCategoryDetailSerializer + + +class ProjectCategoryList(generics.ListAPIView): + + queryset = ProjectCategory.objects.filter(parent=None) + serializer_class = ProjectCategoryDetailSerializer + + +class ProjectPartsList(generics.ListAPIView): + + serializer_class = ProjectPartSerializer + + def get_queryset(self): + project_id = self.kwargs['pk'] + return ProjectPart.objects.filter(project=project_id) From 4a74cd21994fbbfebda36bbba41ead187a507da3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 23:15:17 +1000 Subject: [PATCH 38/42] style errors --- InvenTree/project/serializers.py | 5 +---- InvenTree/project/views.py | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py index 4123968042..f4a0daecec 100644 --- a/InvenTree/project/serializers.py +++ b/InvenTree/project/serializers.py @@ -2,8 +2,6 @@ from rest_framework import serializers from .models import ProjectCategory, Project, ProjectPart -from part.serializers import PartBriefSerializer - class ProjectPartSerializer(serializers.ModelSerializer): @@ -21,7 +19,6 @@ class ProjectBriefSerializer(serializers.ModelSerializer): """ Serializer for displaying brief overview of a project """ - class Meta: model = Project fields = ('pk', @@ -46,7 +43,7 @@ class ProjectCategoryBriefSerializer(serializers.ModelSerializer): class Meta: model = ProjectCategory - fields = ('pk','name','description') + fields = ('pk', 'name', 'description') class ProjectCategoryDetailSerializer(serializers.ModelSerializer): diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py index a92158ff48..46a5fdb0c4 100644 --- a/InvenTree/project/views.py +++ b/InvenTree/project/views.py @@ -2,9 +2,10 @@ from rest_framework import generics from .models import ProjectCategory, Project, ProjectPart from .serializers import ProjectBriefSerializer, ProjectDetailSerializer -from .serializers import ProjectCategoryBriefSerializer, ProjectCategoryDetailSerializer +from .serializers import ProjectCategoryDetailSerializer from .serializers import ProjectPartSerializer + class ProjectDetail(generics.RetrieveAPIView): queryset = Project.objects.all() @@ -17,7 +18,6 @@ class ProjectList(generics.ListAPIView): serializer_class = ProjectBriefSerializer - class ProjectCategoryDetail(generics.RetrieveAPIView): queryset = ProjectCategory.objects.all() From 08fc21eb9baa121976dcb498116770b10b6e820e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 23:22:05 +1000 Subject: [PATCH 39/42] Added ability to mark project part as "output" (generated BY project) --- InvenTree/project/admin.py | 2 +- InvenTree/project/models.py | 4 ++++ InvenTree/project/serializers.py | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/InvenTree/project/admin.py b/InvenTree/project/admin.py index 2d8b43b0d9..9224385d9a 100644 --- a/InvenTree/project/admin.py +++ b/InvenTree/project/admin.py @@ -12,7 +12,7 @@ class ProjectAdmin(admin.ModelAdmin): class ProjectPartAdmin(admin.ModelAdmin): - list_display = ('part', 'project', 'quantity') + list_display = ('part', 'project', 'quantity', 'output') class ProjectRunAdmin(admin.ModelAdmin): diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py index c289f5ebd3..1958540699 100644 --- a/InvenTree/project/models.py +++ b/InvenTree/project/models.py @@ -64,6 +64,10 @@ class ProjectPart(models.Model): default=OVERAGE_ABSOLUTE, choices=OVARAGE_CODES.items()) + # Set if the part is generated by the project, + # rather than being consumed by the project + output = models.BooleanField(default=False) + def __str__(self): return "{quan} x {name}".format( name=self.part.name, diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py index f4a0daecec..0dae3f7cbd 100644 --- a/InvenTree/project/serializers.py +++ b/InvenTree/project/serializers.py @@ -12,7 +12,8 @@ class ProjectPartSerializer(serializers.ModelSerializer): 'project', 'quantity', 'overage', - 'overage_type') + 'overage_type', + 'output') class ProjectBriefSerializer(serializers.ModelSerializer): From 4c07e8d8a0c6579e83a89e7cf9ebae83cf9e6a02 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 23:33:08 +1000 Subject: [PATCH 40/42] Updated part API --- InvenTree/part/serializers.py | 26 +++++--------------------- InvenTree/part/urls.py | 3 +++ InvenTree/part/views.py | 18 ++++++++++++++---- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 523b55f9eb..1ff66264e3 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -3,41 +3,25 @@ from rest_framework import serializers from .models import Part, PartCategory, PartParameter -class ParameterSerializer(serializers.ModelSerializer): +class PartParameterSerializer(serializers.ModelSerializer): """ Serializer for a PartParameter """ class Meta: model = PartParameter fields = ('pk', + 'part', + 'template', 'name', 'value', 'units') -class PartDetailSerializer(serializers.ModelSerializer): +class PartSerializer(serializers.ModelSerializer): """ Serializer for complete detail information of a part. Used when displaying all details of a single component. """ - params = ParameterSerializer(source='parameters', many=True) - - class Meta: - model = Part - fields = ('pk', - 'name', - 'IPN', - 'description', - 'category', - 'stock', - 'params') - - -class PartBriefSerializer(serializers.ModelSerializer): - """ Serializer for displaying overview of a part. - Used e.g. for displaying list of parts in a category. - """ - class Meta: model = Part fields = ('pk', @@ -60,7 +44,7 @@ class PartCategoryBriefSerializer(serializers.ModelSerializer): class PartCategoryDetailSerializer(serializers.ModelSerializer): # List of parts in this category - parts = PartBriefSerializer(many=True) + parts = PartSerializer(many=True) # List of child categories under this one children = PartCategoryBriefSerializer(many=True) diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 601e2d74ee..de6c9c13a8 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -6,6 +6,9 @@ urlpatterns = [ # Single part detail url(r'^(?P[0-9]+)/$', views.PartDetail.as_view()), + # Part parameters list + url(r'^(?P[0-9]+)/parameters/$', views.PartParameters.as_view()), + # Part category detail url(r'^category/(?P[0-9]+)/$', views.PartCategoryDetail.as_view()), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 85451ef00e..a6a4794011 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,20 +1,30 @@ from rest_framework import generics -from .models import PartCategory, Part -from .serializers import PartBriefSerializer, PartDetailSerializer +from .models import PartCategory, Part, PartParameter +from .serializers import PartSerializer from .serializers import PartCategoryBriefSerializer, PartCategoryDetailSerializer +from .serializers import PartParameterSerializer class PartDetail(generics.RetrieveAPIView): queryset = Part.objects.all() - serializer_class = PartDetailSerializer + serializer_class = PartSerializer + + +class PartParameters(generics.ListAPIView): + + def get_queryset(self): + part_id = self.kwargs['pk'] + return PartParameter.objects.filter(part=part_id) + + serializer_class = PartParameterSerializer class PartList(generics.ListAPIView): queryset = Part.objects.all() - serializer_class = PartBriefSerializer + serializer_class = PartSerializer class PartCategoryDetail(generics.RetrieveAPIView): From 5853a5767c2b73d14fd1cd0b8843bda38de5b4c2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 23:36:01 +1000 Subject: [PATCH 41/42] Fix for part category API --- InvenTree/part/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index a6a4794011..81171cf9df 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -39,4 +39,4 @@ class PartCategoryList(generics.ListAPIView): Categories are considered "top-level" if they do not have a parent """ queryset = PartCategory.objects.filter(parent=None) - serializer_class = PartCategoryBriefSerializer + serializer_class = PartCategoryDetailSerializer From eb4ef9de98ba7abdfb9aa2fc1d2c53f8097e6493 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Apr 2017 23:50:25 +1000 Subject: [PATCH 42/42] Fixed import that was not used --- InvenTree/part/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 81171cf9df..dcd5ac54a3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2,7 +2,7 @@ from rest_framework import generics from .models import PartCategory, Part, PartParameter from .serializers import PartSerializer -from .serializers import PartCategoryBriefSerializer, PartCategoryDetailSerializer +from .serializers import PartCategoryDetailSerializer from .serializers import PartParameterSerializer