From 7c45f8dc24b56f1f635012af6c1ba858b6119c60 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 08:11:29 +1000 Subject: [PATCH 01/13] Remove absolute URL encoding - Should now work on RTD --- docs/index.rst | 4 ++-- docs/introduction.rst | 14 ++++++++++++-- docs/reference.rst | 7 +++++++ docs/templates/layout.html | 7 ------- 4 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 docs/reference.rst delete mode 100644 docs/templates/layout.html diff --git a/docs/index.rst b/docs/index.rst index 114ecaf27b..47c0b88b5e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,6 @@ This documentation is auto-generated from the `InvenTree codebase + InvenTree Modules + API Reference \ No newline at end of file diff --git a/docs/introduction.rst b/docs/introduction.rst index e117010749..f33917212a 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -6,11 +6,21 @@ InvenTree is an open source inventory management system which provides powerful The core of the InvenTree software is a Python/Django database backend whi -Django Apps -=========== +**Django Apps** The InvenTree Django ecosystem provides the following 'apps' for core functionality: +.. toctree:: + :titlesonly: + :maxdepth: 1 + :caption: App Modules: + + docs/InvenTree/index + docs/build/index + docs/company/index + docs/part/index + docs/stock/index + * `InvenTree `_ - High level management functions * `Build `_ - Part build projects * `Company `_ - Company management (suppliers / customers) diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000000..de81c25e26 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,7 @@ +API Reference Index +=================== + +The complete reference indexes are found below: + +* :ref:`modindex` +* :ref:`genindex` \ No newline at end of file diff --git a/docs/templates/layout.html b/docs/templates/layout.html deleted file mode 100644 index 620814d1b3..0000000000 --- a/docs/templates/layout.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "!layout.html" %} - - {% block menu %} - {{ super() }} - Module Index - Index - {% endblock %} \ No newline at end of file From bf76a6024ebf4783f64e0d8387549dac98d11e63 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 28 Apr 2019 08:39:41 +1000 Subject: [PATCH 02/13] Update README.md Much improve so better --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 44e35d7c59..5365a85c07 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,54 @@ # 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 +InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action procses and does not require a complex system of work orders or stock transactions. + +However, complex business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information. + +## User Documentation + +**TODO:** User documentation will be provided on a linked ```.github.io``` site, and will document the various InvenTree functionality + +## Code Documentation + +For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated) + + +## Getting Started + 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` -You can then continue running `make setup` (which will be replaced by a proper setup.py soon). This will do the following: +A makefile is provided for project configuration: -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 +### Install -This command can also be used to update the installation if changes have been made to the database configuration. +Run `make install` to ensure that all required pip packages are installed (see `requirements.txt`). This step will also generate a `SECRET_KEY.txt` file (unless one already exists) for Django authentication. -To create an initial user account, run the command `make superuser`. +### Migrate -## Documentation -For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated) +Run `make migrate` to perform all pending database migrations to ensure the database schema is up to date. -## Coding 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`. +**Note:** Run this step once after `make install` to create the initial empty database. + +### Superuser + +Run `make superuser` to create an admin account for the database + +### Test + +Run `make test` to run all code tests + +### Style + +Run `make style` to check the codebase against PEP coding standards (uses Flake) + +## Contributing + +### Testing + +Any new functionality should be submitted with matching test cases (using the Django testing framework). Tests should at bare minimum ensure that any new Python code is covered by the integrated coverage testing. Tests can be run using `make test`. + +### Coding Style + +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. From 2108ee2eb7a58f951b3771c62c74818d60686ff4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 08:42:17 +1000 Subject: [PATCH 03/13] Update makefile --- .travis.yml | 3 ++- .../migrations/0011_auto_20190428_0841.py | 19 +++++++++++++++++++ .../migrations/0009_auto_20190428_0841.py | 19 +++++++++++++++++++ Makefile | 5 ++--- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 InvenTree/part/migrations/0011_auto_20190428_0841.py create mode 100644 InvenTree/stock/migrations/0009_auto_20190428_0841.py diff --git a/.travis.yml b/.travis.yml index 124ed02da0..37ad7c425c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,8 @@ addons: -sqlite3 before_install: - - make setup + - make install + - make migrate script: - make coverage diff --git a/InvenTree/part/migrations/0011_auto_20190428_0841.py b/InvenTree/part/migrations/0011_auto_20190428_0841.py new file mode 100644 index 0000000000..0146b6795a --- /dev/null +++ b/InvenTree/part/migrations/0011_auto_20190428_0841.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-04-27 22:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0010_auto_20190417_0045'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierpart', + name='supplier', + field=models.ForeignKey(limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='company.Company'), + ), + ] diff --git a/InvenTree/stock/migrations/0009_auto_20190428_0841.py b/InvenTree/stock/migrations/0009_auto_20190428_0841.py new file mode 100644 index 0000000000..4e22dad76b --- /dev/null +++ b/InvenTree/stock/migrations/0009_auto_20190428_0841.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2019-04-27 22:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0008_auto_20190417_1819'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='location', + field=models.ForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation'), + ), + ] diff --git a/Makefile b/Makefile index d675749c5c..38081570e4 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,8 @@ install: pip install -U -r requirements.txt python InvenTree/keygen.py -setup: install migrate +superuser: + python InvenTree/manage.py createsuperuser style: flake8 InvenTree @@ -37,5 +38,3 @@ documentation: pip install -U -r docs/requirements.txt cd docs & make html -superuser: - python InvenTree/manage.py createsuperuser From 9b77340782fc858259ca8fbc745dce766365057b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 08:55:29 +1000 Subject: [PATCH 04/13] Change field ordering for SupplierPart form - Import fields first! --- InvenTree/part/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 040444093d..1157012196 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -101,13 +101,13 @@ class EditSupplierPartForm(HelperForm): class Meta: model = SupplierPart fields = [ + 'part', 'supplier', 'SKU', - 'part', 'description', - 'URL', 'manufacturer', 'MPN', + 'URL', 'note', 'single_price', 'base_cost', From e5fc43a00fae733574a0e201b3a3aae79251e5f9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 09:00:54 +1000 Subject: [PATCH 05/13] Fix improper references to template_name for ajax forms - Should be ajax_template_name --- InvenTree/part/views.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index db9f66994c..6db1edfc8e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -158,7 +158,6 @@ class PartEdit(AjaxUpdateView): """ View for editing Part object """ model = Part - template_name = 'part/edit.html' form_class = EditPartForm ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit Part Properties' @@ -246,7 +245,6 @@ class PartDelete(AjaxDeleteView): """ View to delete a Part object """ model = Part - template_name = 'part/delete.html' ajax_template_name = 'part/partial_delete.html' ajax_form_title = 'Confirm Part Deletion' context_object_name = 'part' @@ -270,7 +268,6 @@ class CategoryDetail(DetailView): class CategoryEdit(AjaxUpdateView): """ Update view to edit a PartCategory """ model = PartCategory - template_name = 'part/category_edit.html' form_class = EditCategoryForm ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit Part Category' @@ -286,7 +283,7 @@ class CategoryEdit(AjaxUpdateView): class CategoryDelete(AjaxDeleteView): """ Delete view to delete a PartCategory """ model = PartCategory - template_name = 'part/category_delete.html' + ajax_template_name = 'part/category_delete.html' context_object_name = 'category' success_url = '/part/' @@ -302,7 +299,6 @@ class CategoryCreate(AjaxCreateView): ajax_form_action = reverse_lazy('category-create') ajax_form_title = 'Create new part category' ajax_template_name = 'modal_form.html' - template_name = 'part/category_new.html' form_class = EditCategoryForm def get_context_data(self, **kwargs): @@ -345,7 +341,6 @@ class BomItemCreate(AjaxCreateView): """ Create view for making a new BomItem object """ model = BomItem form_class = EditBomItemForm - template_name = 'part/bom-create.html' ajax_template_name = 'modal_form.html' ajax_form_title = 'Create BOM item' @@ -372,7 +367,6 @@ class BomItemEdit(AjaxUpdateView): model = BomItem form_class = EditBomItemForm - template_name = 'part/bom-edit.html' ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit BOM item' @@ -380,7 +374,7 @@ class BomItemEdit(AjaxUpdateView): class BomItemDelete(AjaxDeleteView): """ Delete view for removing BomItem """ model = BomItem - template_name = 'part/bom-delete.html' + ajax_template_name = 'part/bom-delete.html' context_object_name = 'item' ajax_form_title = 'Confim BOM item deletion' @@ -397,7 +391,6 @@ class SupplierPartEdit(AjaxUpdateView): """ Update view for editing SupplierPart """ model = SupplierPart - template_name = 'company/partedit.html' context_object_name = 'part' form_class = EditSupplierPartForm ajax_template_name = 'modal_form.html' @@ -440,4 +433,4 @@ class SupplierPartDelete(AjaxDeleteView): """ Delete view for removing a SupplierPart """ model = SupplierPart success_url = '/supplier/' - template_name = 'company/partdelete.html' + ajax_template_name = 'company/partdelete.html' From cd438f05699656a01d7ffe3f405fb6e88e56114a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 09:53:42 +1000 Subject: [PATCH 06/13] Add generic method for retriving GET or POST params --- InvenTree/InvenTree/views.py | 16 ++++++++++++++++ InvenTree/part/forms.py | 2 ++ InvenTree/part/views.py | 22 +++++++++++++--------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 551f624cb9..ffd757ebdc 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -69,6 +69,22 @@ class AjaxMixin(object): ajax_form_action = '' ajax_form_title = '' + def get_param(self, name, method='GET'): + """ Get a request query parameter value from URL e.g. ?part=3 + + Args: + name: Variable name e.g. 'part' + method: Request type ('GET' or 'POST') + + Returns: + Value of the supplier parameter or None if parameter is not available + """ + + if method == 'POST': + return self.request.POST.get(name, None) + else: + return self.request.GET.get(name, None) + def get_data(self): """ Get extra context data (default implementation is empty dict) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 1157012196..bba8c3e973 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -92,6 +92,8 @@ class EditBomItemForm(HelperForm): 'quantity', 'note' ] + + # Prevent editing of the part associated with this BomItem widgets = {'part': forms.HiddenInput()} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6db1edfc8e..7667b3908a 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -406,6 +406,7 @@ class SupplierPartCreate(AjaxCreateView): ajax_form_title = 'Create new Supplier Part' context_object_name = 'part' + def get_initial(self): """ Provide initial data for new SupplierPart: @@ -414,18 +415,21 @@ class SupplierPartCreate(AjaxCreateView): """ initials = super(SupplierPartCreate, self).get_initial().copy() - supplier_id = self.request.GET.get('supplier', None) - part_id = self.request.GET.get('part', None) + supplier_id = self.get_param('supplier') + part_id = self.get_param('part') if supplier_id: - initials['supplier'] = get_object_or_404(Company, pk=supplier_id) - # TODO - # self.fields['supplier'].disabled = True + try: + initials['supplier'] = Company.objects.get(pk=supplier_id) + except Company.DoesNotExist: + initials['supplier'] = None + if part_id: - initials['part'] = get_object_or_404(Part, pk=part_id) - # TODO - # self.fields['part'].disabled = True - + try: + initials['part'] = Part.objects.get(pk=part_id) + except Part.DoesNotExist: + initials['part'] = None + return initials From 96a15453c4e113e65e203d3250617a414892f594 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 10:35:17 +1000 Subject: [PATCH 07/13] Simplify AjaxCreateView - Screams in reading the documentation --- InvenTree/InvenTree/views.py | 44 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index ffd757ebdc..ee4a7bbacb 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -150,39 +150,37 @@ class AjaxCreateView(AjaxMixin, CreateView): """ def get(self, request, *args, **kwargs): + """ Creates form with initial data, and renders JSON response """ - response = super(CreateView, self).get(request, *args, **kwargs) + super(CreateView, self).get(request, *args, **kwargs) - if request.is_ajax(): - # Initialize a a new form - form = self.form_class(initial=self.get_initial()) - - return self.renderJsonResponse(request, form) - - else: - return response + form = self.get_form() + return self.renderJsonResponse(request, form) def post(self, request, *args, **kwargs): - form = self.form_class(data=request.POST, files=request.FILES) + """ Responds to form POST. Validates POST data and returns status info. - if request.is_ajax(): + Steps: + 1. Validate POST form data + 2. If valid, save form + 3. Return status info (success / failure) + """ + form = self.get_form() - data = { - 'form_valid': form.is_valid(), - } + # Extra JSON data sent alongside form + data = { + 'form_valid': form.is_valid(), + } - if form.is_valid(): - obj = form.save() + if form.is_valid(): + obj = form.save() - # Return the PK of the newly-created object - data['pk'] = obj.pk + # Return the PK of the newly-created object + data['pk'] = obj.pk - data['url'] = obj.get_absolute_url() + data['url'] = obj.get_absolute_url() - return self.renderJsonResponse(request, form, data) - - else: - return super(CreateView, self).post(request, *args, **kwargs) + return self.renderJsonResponse(request, form, data) class AjaxUpdateView(AjaxMixin, UpdateView): From 72ff25e0c138ff3c84cfce6b23e18d1c9790f757 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 10:39:46 +1000 Subject: [PATCH 08/13] Simplify AjaxUpdateView - Using get_form() is MUCH simpler --- InvenTree/InvenTree/views.py | 37 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index ee4a7bbacb..0aa0e38e63 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -184,7 +184,6 @@ class AjaxCreateView(AjaxMixin, CreateView): class AjaxUpdateView(AjaxMixin, UpdateView): - """ An 'AJAXified' UpdateView for updating an object in the db - Returns form in JSON format (for delivery to a modal window) - Handles repeated form validation (via AJAX) until the form is valid @@ -192,35 +191,31 @@ class AjaxUpdateView(AjaxMixin, UpdateView): def get(self, request, *args, **kwargs): - html_response = super(UpdateView, self).get(request, *args, **kwargs) + super(UpdateView, self).get(request, *args, **kwargs) - if request.is_ajax(): - form = self.form_class(instance=self.get_object()) + form = self.get_form() + # form = self.form_class(instance=self.get_object()) - return self.renderJsonResponse(request, form) - - else: - return html_response + return self.renderJsonResponse(request, form) def post(self, request, *args, **kwargs): - form = self.form_class(instance=self.get_object(), data=request.POST, files=request.FILES) + super(UpdateView, self).post(request, *args, **kwargs) - if request.is_ajax(): + form = self.get_form() - data = {'form_valid': form.is_valid()} + data = { + 'form_valid': form.is_valid() + } - if form.is_valid(): - obj = form.save() + if form.is_valid(): + obj = form.save() + + # Include context data about the updated object + data['pk'] = obj.id + data['url'] = obj.get_absolute_url() - data['pk'] = obj.id - data['url'] = obj.get_absolute_url() - - response = self.renderJsonResponse(request, form, data) - return response - - else: - return super(UpdateView, self).post(request, *args, **kwargs) + return self.renderJsonResponse(request, form, data) class AjaxDeleteView(AjaxMixin, DeleteView): From ff068de16d597e7c56cdee6bbde35e6cedf3abcb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 10:41:54 +1000 Subject: [PATCH 09/13] Simplify AjaxDeleteView --- InvenTree/InvenTree/views.py | 46 ++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 0aa0e38e63..e7dad657ba 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -227,39 +227,33 @@ class AjaxDeleteView(AjaxMixin, DeleteView): def get(self, request, *args, **kwargs): - html_response = super(DeleteView, self).get(request, *args, **kwargs) + super(DeleteView, self).get(request, *args, **kwargs) - if request.is_ajax(): + data = { + 'id': self.get_object().id, + 'delete': False, + 'title': self.ajax_form_title, + 'html_data': render_to_string( + self.ajax_template_name, + self.get_context_data(), + request=request) + } - data = {'id': self.get_object().id, - 'delete': False, - 'title': self.ajax_form_title, - 'html_data': render_to_string(self.ajax_template_name, - self.get_context_data(), - request=request) - } - - return JsonResponse(data) - - else: - return html_response + return JsonResponse(data) def post(self, request, *args, **kwargs): - if request.is_ajax(): + obj = self.get_object() + pk = obj.id + obj.delete() - obj = self.get_object() - pk = obj.id - obj.delete() - - data = {'id': pk, - 'delete': True} - - return self.renderJsonResponse(request, data=data) - - else: - return super(DeleteView, self).post(request, *args, **kwargs) + data = { + 'id': pk, + 'delete': True + } + return self.renderJsonResponse(request, data=data) + class IndexView(TemplateView): """ View for InvenTree index page """ From 101ce53cd30106956860d8e67805276121f41074 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 10:46:07 +1000 Subject: [PATCH 10/13] Update docstrings for InvenTree/views.py --- InvenTree/InvenTree/views.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index e7dad657ba..21adcd0021 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -160,10 +160,9 @@ class AjaxCreateView(AjaxMixin, CreateView): def post(self, request, *args, **kwargs): """ Responds to form POST. Validates POST data and returns status info. - Steps: - 1. Validate POST form data - 2. If valid, save form - 3. Return status info (success / failure) + - Validate POST form data + - If valid, save form + - Return status info (success / failure) """ form = self.get_form() @@ -190,6 +189,11 @@ class AjaxUpdateView(AjaxMixin, UpdateView): """ def get(self, request, *args, **kwargs): + """ Respond to GET request. + + - Populates form with object data + - Renders form to JSON and returns to client + """ super(UpdateView, self).get(request, *args, **kwargs) @@ -199,6 +203,13 @@ class AjaxUpdateView(AjaxMixin, UpdateView): return self.renderJsonResponse(request, form) def post(self, request, *args, **kwargs): + """ Respond to POST request. + + - Updates model with POST field data + - Performs form and object validation + - If errors exist, re-render the form + - Otherwise, return sucess status + """ super(UpdateView, self).post(request, *args, **kwargs) @@ -226,6 +237,11 @@ class AjaxDeleteView(AjaxMixin, DeleteView): """ def get(self, request, *args, **kwargs): + """ Respond to GET request + + - Render a DELETE confirmation form to JSON + - Return rendered form to client + """ super(DeleteView, self).get(request, *args, **kwargs) @@ -242,6 +258,11 @@ class AjaxDeleteView(AjaxMixin, DeleteView): return JsonResponse(data) def post(self, request, *args, **kwargs): + """ Respond to POST request + + - DELETE the object + - Render success message to JSON and return to client + """ obj = self.get_object() pk = obj.id @@ -253,7 +274,7 @@ class AjaxDeleteView(AjaxMixin, DeleteView): } return self.renderJsonResponse(request, data=data) - + class IndexView(TemplateView): """ View for InvenTree index page """ From 624c5094c5851bfbef63569ab5ce393f25238010 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 10:57:32 +1000 Subject: [PATCH 11/13] Prevent editing of SupplierPart fields if initial values are provided --- InvenTree/part/views.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 7667b3908a..b8762f556d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -406,6 +406,23 @@ class SupplierPartCreate(AjaxCreateView): ajax_form_title = 'Create new Supplier Part' context_object_name = 'part' + def get_form(self): + form = super(AjaxCreateView, self).get_form() + + # Was the form supplied with initial data? + + print("Initial:", form.initial) + print(dir(form)) + + if form.initial.get('supplier', None): + # Hide the supplier field + form.fields['supplier'].widget.attrs['disabled'] = True + + if form.initial.get('part', None): + # Hide the part field + form.fields['part'].widget.attrs['disabled'] = True + + return form def get_initial(self): """ Provide initial data for new SupplierPart: From a9fbbc3a37cf44c85db14d0a4a521cc9bebb8ca7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 11:09:19 +1000 Subject: [PATCH 12/13] Remove references to get_object_or_404 - Properly handle DoesNotExist errors --- InvenTree/InvenTree/views.py | 7 +++---- InvenTree/build/views.py | 5 ++++- InvenTree/part/api.py | 27 +++++++++++++++------------ InvenTree/part/views.py | 30 ++++++++++++++++++++++++------ InvenTree/stock/api.py | 27 +++++++++++++++------------ InvenTree/stock/views.py | 24 +++++++++++++++++------- 6 files changed, 78 insertions(+), 42 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 21adcd0021..26303a6549 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -150,7 +150,7 @@ class AjaxCreateView(AjaxMixin, CreateView): """ def get(self, request, *args, **kwargs): - """ Creates form with initial data, and renders JSON response """ + """ Creates form with initial data, and renders JSON response """ super(CreateView, self).get(request, *args, **kwargs) @@ -161,7 +161,7 @@ class AjaxCreateView(AjaxMixin, CreateView): """ Responds to form POST. Validates POST data and returns status info. - Validate POST form data - - If valid, save form + - If valid, save form - Return status info (success / failure) """ form = self.get_form() @@ -198,8 +198,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView): super(UpdateView, self).get(request, *args, **kwargs) form = self.get_form() - # form = self.form_class(instance=self.get_object()) - + return self.renderJsonResponse(request, form) def post(self, request, *args, **kwargs): diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index c8d40e80e6..dc7b3141c3 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -101,7 +101,10 @@ class BuildCreate(AjaxCreateView): part_id = self.request.GET.get('part', None) if part_id: - initials['part'] = get_object_or_404(Part, pk=part_id) + try: + initials['part'] = Part.objects.get(pk=part_id) + except Part.DoesNotExist: + pass return initials diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 8f93cede7c..46a15cfc62 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -11,7 +11,6 @@ from rest_framework import generics, permissions from django.db.models import Q from django.conf.urls import url, include -from django.shortcuts import get_object_or_404 from .models import Part, PartCategory, BomItem from .models import SupplierPart, SupplierPriceBreak @@ -99,20 +98,24 @@ class PartList(generics.ListCreateAPIView): parts_list = Part.objects.all() if cat_id: - category = get_object_or_404(PartCategory, pk=cat_id) + try: + category = PartCategory.objects.get(pk=cat_id) + + # Filter by the supplied category + flt = Q(category=cat_id) - # Filter by the supplied category - flt = Q(category=cat_id) + if self.request.query_params.get('include_child_categories', None): + childs = category.getUniqueChildren() + for child in childs: + # Ignore the top-level category (already filtered) + if str(child) == str(cat_id): + continue + flt |= Q(category=child) - if self.request.query_params.get('include_child_categories', None): - childs = category.getUniqueChildren() - for child in childs: - # Ignore the top-level category (already filtered) - if str(child) == str(cat_id): - continue - flt |= Q(category=child) + parts_list = parts_list.filter(flt) - parts_list = parts_list.filter(flt) + except PartCategory.DoesNotExist: + pass return parts_list diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b8762f556d..eb7d9ea0f3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -84,7 +84,10 @@ class PartCreate(AjaxCreateView): cat_id = self.get_category_id() if cat_id: - context['category'] = get_object_or_404(PartCategory, pk=cat_id) + try: + context['category'] = PartCategory.objects.get(pk=cat_id) + except PartCategory.DoesNotExist: + pass return context @@ -111,7 +114,10 @@ class PartCreate(AjaxCreateView): initials = super(PartCreate, self).get_initial() if self.get_category_id(): - initials['category'] = get_object_or_404(PartCategory, pk=self.get_category_id()) + try: + initials['category'] = PartCategory.objects.get(pk=self.get_category_id()) + except PartCategory.DoesNotExist: + pass return initials @@ -275,7 +281,10 @@ class CategoryEdit(AjaxUpdateView): def get_context_data(self, **kwargs): context = super(CategoryEdit, self).get_context_data(**kwargs).copy() - context['category'] = get_object_or_404(PartCategory, pk=self.kwargs['pk']) + try: + context['category'] = PartCategory.objects.get(pk=self.kwargs['pk']) + except: + pass return context @@ -311,7 +320,10 @@ class CategoryCreate(AjaxCreateView): parent_id = self.request.GET.get('category', None) if parent_id: - context['category'] = get_object_or_404(PartCategory, pk=parent_id) + try: + context['category'] = PartCategory.objects.get(pk=parent_id) + except PartCategory.DoesNotExist: + pass return context @@ -325,7 +337,10 @@ class CategoryCreate(AjaxCreateView): parent_id = self.request.GET.get('category', None) if parent_id: - initials['parent'] = get_object_or_404(PartCategory, pk=parent_id) + try: + initials['parent'] = PartCategory.objects.get(pk=parent_id) + except PartCategory.DoesNotExist: + pass return initials @@ -357,7 +372,10 @@ class BomItemCreate(AjaxCreateView): parent_id = self.request.GET.get('parent', None) if parent_id: - initials['part'] = get_object_or_404(Part, pk=parent_id) + try: + initials['part'] = Part.objects.get(pk=parent_id) + except Part.DoesNotExist: + pass return initials diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index c352605ad2..8604f54c8f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -7,7 +7,6 @@ from django_filters import NumberFilter from django.conf.urls import url, include from django.db.models import Q -from django.shortcuts import get_object_or_404 from .models import StockLocation, StockItem from .models import StockItemTracking @@ -238,20 +237,24 @@ class StockList(generics.ListCreateAPIView): stock_list = StockItem.objects.all() if loc_id: - location = get_object_or_404(StockLocation, pk=loc_id) + try: + location = StockLocation.objects.get(pk=loc_id) - # Filter by the supplied category - flt = Q(location=loc_id) + # Filter by the supplied category + flt = Q(location=loc_id) - if self.request.query_params.get('include_child_locations', None): - childs = location.getUniqueChildren() - for child in childs: - # Ignore the top-level category (already filtered!) - if str(child) == str(loc_id): - continue - flt |= Q(location=child) + if self.request.query_params.get('include_child_locations', None): + childs = location.getUniqueChildren() + for child in childs: + # Ignore the top-level category (already filtered!) + if str(child) == str(loc_id): + continue + flt |= Q(location=child) - stock_list = stock_list.filter(flt) + stock_list = stock_list.filter(flt) + + except StockLocation.DoesNotExist: + pass return stock_list diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 70d3f7ded3..b9c258f79d 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -5,8 +5,6 @@ Django views for interacting with Stock app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.shortcuts import get_object_or_404 - from django.views.generic import DetailView, ListView from django.forms.models import model_to_dict @@ -106,7 +104,10 @@ class StockLocationCreate(AjaxCreateView): loc_id = self.request.GET.get('location', None) if loc_id: - initials['parent'] = get_object_or_404(StockLocation, pk=loc_id) + try: + initials['parent'] = StockLocation.objects.get(pk=loc_id) + except StockLocation.DoesNotExist: + pass return initials @@ -126,6 +127,8 @@ class StockItemCreate(AjaxCreateView): ajax_form_title = 'Create new Stock Item' def get_initial(self): + """ Provide initial data to create a new StockItem object + """ # Is the client attempting to copy an existing stock item? item_to_copy = self.request.GET.get('copy', None) @@ -144,15 +147,22 @@ class StockItemCreate(AjaxCreateView): part_id = self.request.GET.get('part', None) loc_id = self.request.GET.get('location', None) + # Part field has been specified if part_id: - part = get_object_or_404(Part, pk=part_id) - if part: - initials['part'] = get_object_or_404(Part, pk=part_id) + try: + part = Part.objects.get(pk=part_id) + initials['part'] = part initials['location'] = part.default_location initials['supplier_part'] = part.default_supplier + except Part.DoesNotExist: + pass + # Location has been specified if loc_id: - initials['location'] = get_object_or_404(StockLocation, pk=loc_id) + try: + initials['location'] = StockLocation.objects.get(pk=loc_id) + except StockLocation.DoesNotExist: + pass return initials From 1171953e65de65e567f85e54911bb3e7342900dd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 11:24:26 +1000 Subject: [PATCH 13/13] Limit SupplierPart options in CreateBomItem form - If Part is selected, limit the options in supplier_part field - Only allow supplier_parts which map back to the same part --- InvenTree/part/views.py | 5 ----- InvenTree/stock/views.py | 25 +++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index eb7d9ea0f3..d0f3a1ac01 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -426,11 +426,6 @@ class SupplierPartCreate(AjaxCreateView): def get_form(self): form = super(AjaxCreateView, self).get_form() - - # Was the form supplied with initial data? - - print("Initial:", form.initial) - print(dir(form)) if form.initial.get('supplier', None): # Hide the supplier field diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index b9c258f79d..a1f0d2335c 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -126,6 +126,27 @@ class StockItemCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' ajax_form_title = 'Create new Stock Item' + def get_form(self): + """ Get form for StockItem creation. + Overrides the default get_form() method to intelligently limit + ForeignKey choices based on other selections + """ + + form = super(AjaxCreateView, self).get_form() + + # If the user has selected a Part, limit choices for SupplierPart + if form['part'].value() is not None: + part = form['part'].value() + parts = form.fields['supplier_part'].queryset + parts = parts.filter(part=part) + form.fields['supplier_part'].queryset = parts + + # Otherwise if the user has selected a SupplierPart, we know what Part they meant! + elif form['supplier_part'].value() is not None: + pass + + return form + def get_initial(self): """ Provide initial data to create a new StockItem object """ @@ -188,7 +209,7 @@ class StockItemDelete(AjaxDeleteView): model = StockItem success_url = '/stock/' - template_name = 'stock/item_delete.html' + ajax_template_name = 'stock/item_delete.html' context_object_name = 'item' ajax_form_title = 'Delete Stock Item' @@ -200,7 +221,7 @@ class StockItemMove(AjaxUpdateView): """ model = StockItem - template_name = 'modal_form.html' + ajax_template_name = 'modal_form.html' context_object_name = 'item' ajax_form_title = 'Move Stock Item' form_class = MoveStockItemForm