mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 21:25:42 +00:00 
			
		
		
		
	Merge pull request #675 from SchrodingersGat/po-attachment
Po attachment
This commit is contained in:
		| @@ -4,8 +4,11 @@ Generic models which provide extra functionality over base Django model types. | |||||||
|  |  | ||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals | ||||||
|  |  | ||||||
|  | import os | ||||||
|  |  | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
| from django.db.models.signals import pre_delete | from django.db.models.signals import pre_delete | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| @@ -15,6 +18,51 @@ from mptt.models import MPTTModel, TreeForeignKey | |||||||
| from .validators import validate_tree_name | from .validators import validate_tree_name | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def rename_attachment(instance, filename): | ||||||
|  |     """ | ||||||
|  |     Function for renaming an attachment file. | ||||||
|  |     The subdirectory for the uploaded file is determined by the implementing class. | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |         instance: Instance of a PartAttachment object | ||||||
|  |         filename: name of uploaded file | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         path to store file, format: '<subdir>/<id>/filename' | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # Construct a path to store a file attachment for a given model type | ||||||
|  |     return os.path.join(instance.getSubdir(), filename) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class InvenTreeAttachment(models.Model): | ||||||
|  |     """ Provides an abstracted class for managing file attachments. | ||||||
|  |  | ||||||
|  |     Attributes: | ||||||
|  |         attachment: File | ||||||
|  |         comment: String descriptor for the attachment | ||||||
|  |     """ | ||||||
|  |     def getSubdir(self): | ||||||
|  |         """ | ||||||
|  |         Return the subdirectory under which attachments should be stored. | ||||||
|  |         Note: Re-implement this for each subclass of InvenTreeAttachment | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return "attachments" | ||||||
|  |  | ||||||
|  |     attachment = models.FileField(upload_to=rename_attachment, | ||||||
|  |                                   help_text=_('Select file to attach')) | ||||||
|  |  | ||||||
|  |     comment = models.CharField(max_length=100, help_text=_('File comment')) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def basename(self): | ||||||
|  |         return os.path.basename(self.attachment.name) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         abstract = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class InvenTreeTree(MPTTModel): | class InvenTreeTree(MPTTModel): | ||||||
|     """ Provides an abstracted self-referencing tree model for data categories. |     """ Provides an abstracted self-referencing tree model for data categories. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -100,7 +100,7 @@ InvenTree | {% trans "Company" %} - {{ company.name }} | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     $("#company-order-2").click(function() { |     $("#company-order-2").click(function() { | ||||||
|         launchModalForm("{% url 'purchase-order-create' %}", |         launchModalForm("{% url 'po-create' %}", | ||||||
|         { |         { | ||||||
|             data: { |             data: { | ||||||
|                 supplier: {{ company.id }}, |                 supplier: {{ company.id }}, | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
|     function newOrder() { |     function newOrder() { | ||||||
|         launchModalForm("{% url 'purchase-order-create' %}", |         launchModalForm("{% url 'po-create' %}", | ||||||
|         { |         { | ||||||
|             data: { |             data: { | ||||||
|                 supplier: {{ company.id }}, |                 supplier: {{ company.id }}, | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -13,7 +13,7 @@ from mptt.fields import TreeNodeChoiceField | |||||||
| from InvenTree.forms import HelperForm | from InvenTree.forms import HelperForm | ||||||
|  |  | ||||||
| from stock.models import StockLocation | from stock.models import StockLocation | ||||||
| from .models import PurchaseOrder, PurchaseOrderLineItem | from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment | ||||||
|  |  | ||||||
|  |  | ||||||
| class IssuePurchaseOrderForm(HelperForm): | class IssuePurchaseOrderForm(HelperForm): | ||||||
| @@ -74,6 +74,18 @@ class EditPurchaseOrderForm(HelperForm): | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EditPurchaseOrderAttachmentForm(HelperForm): | ||||||
|  |     """ Form for editing a PurchaseOrderAttachment object """ | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = PurchaseOrderAttachment | ||||||
|  |         fields = [ | ||||||
|  |             'order', | ||||||
|  |             'attachment', | ||||||
|  |             'comment' | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EditPurchaseOrderLineItemForm(HelperForm): | class EditPurchaseOrderLineItemForm(HelperForm): | ||||||
|     """ Form for editing a PurchaseOrderLineItem object """ |     """ Form for editing a PurchaseOrderLineItem object """ | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								InvenTree/order/migrations/0016_purchaseorderattachment.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								InvenTree/order/migrations/0016_purchaseorderattachment.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | # Generated by Django 2.2.9 on 2020-03-22 07:01 | ||||||
|  |  | ||||||
|  | import InvenTree.models | ||||||
|  | from django.db import migrations, models | ||||||
|  | import django.db.models.deletion | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('order', '0015_auto_20200201_2346'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='PurchaseOrderAttachment', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('attachment', models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment)), | ||||||
|  |                 ('comment', models.CharField(help_text='File comment', max_length=100)), | ||||||
|  |                 ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='order.PurchaseOrder')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'abstract': False, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -14,6 +14,7 @@ from django.utils.translation import ugettext as _ | |||||||
|  |  | ||||||
| from markdownx.models import MarkdownxField | from markdownx.models import MarkdownxField | ||||||
|  |  | ||||||
|  | import os | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| from stock.models import StockItem | from stock.models import StockItem | ||||||
| @@ -21,6 +22,7 @@ from company.models import Company, SupplierPart | |||||||
|  |  | ||||||
| from InvenTree.helpers import decimal2string | from InvenTree.helpers import decimal2string | ||||||
| from InvenTree.status_codes import OrderStatus | from InvenTree.status_codes import OrderStatus | ||||||
|  | from InvenTree.models import InvenTreeAttachment | ||||||
|  |  | ||||||
|  |  | ||||||
| class Order(models.Model): | class Order(models.Model): | ||||||
| @@ -136,7 +138,7 @@ class PurchaseOrder(Order): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def get_absolute_url(self): |     def get_absolute_url(self): | ||||||
|         return reverse('purchase-order-detail', kwargs={'pk': self.id}) |         return reverse('po-detail', kwargs={'pk': self.id}) | ||||||
|  |  | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def add_line_item(self, supplier_part, quantity, group=True, reference=''): |     def add_line_item(self, supplier_part, quantity, group=True, reference=''): | ||||||
| @@ -239,6 +241,17 @@ class PurchaseOrder(Order): | |||||||
|             self.complete_order()  # This will save the model |             self.complete_order()  # This will save the model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PurchaseOrderAttachment(InvenTreeAttachment): | ||||||
|  |     """ | ||||||
|  |     Model for storing file attachments against a PurchaseOrder object | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def getSubdir(self): | ||||||
|  |         return os.path.join("po_files", str(self.order.id)) | ||||||
|  |  | ||||||
|  |     order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name="attachments") | ||||||
|  |  | ||||||
|  |  | ||||||
| class OrderLineItem(models.Model): | class OrderLineItem(models.Model): | ||||||
|     """ Abstract model for an order line item |     """ Abstract model for an order line item | ||||||
|      |      | ||||||
|   | |||||||
| @@ -107,7 +107,7 @@ InvenTree | {{ order }} | |||||||
|  |  | ||||||
| {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} | {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} | ||||||
| $("#place-order").click(function() { | $("#place-order").click(function() { | ||||||
|     launchModalForm("{% url 'purchase-order-issue' order.id %}", |     launchModalForm("{% url 'po-issue' order.id %}", | ||||||
|     { |     { | ||||||
|         reload: true, |         reload: true, | ||||||
|     }); |     }); | ||||||
| @@ -115,7 +115,7 @@ $("#place-order").click(function() { | |||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
| $("#edit-order").click(function() { | $("#edit-order").click(function() { | ||||||
|     launchModalForm("{% url 'purchase-order-edit' order.id %}", |     launchModalForm("{% url 'po-edit' order.id %}", | ||||||
|         { |         { | ||||||
|             reload: true, |             reload: true, | ||||||
|         } |         } | ||||||
| @@ -123,7 +123,7 @@ $("#edit-order").click(function() { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| $("#cancel-order").click(function() { | $("#cancel-order").click(function() { | ||||||
|     launchModalForm("{% url 'purchase-order-cancel' order.id %}", { |     launchModalForm("{% url 'po-cancel' order.id %}", { | ||||||
|         reload: true, |         reload: true, | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ | |||||||
| {% if editing %} | {% if editing %} | ||||||
| {% else %} | {% else %} | ||||||
| $("#edit-notes").click(function() { | $("#edit-notes").click(function() { | ||||||
|     location.href = "{% url 'purchase-order-notes' order.id %}?edit=1"; |     location.href = "{% url 'po-notes' order.id %}?edit=1"; | ||||||
| }); | }); | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										81
									
								
								InvenTree/order/templates/order/po_attachments.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								InvenTree/order/templates/order/po_attachments.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | {% extends "order/order_base.html" %} | ||||||
|  |  | ||||||
|  | {% load inventree_extras %} | ||||||
|  | {% load i18n %} | ||||||
|  | {% load static %} | ||||||
|  |  | ||||||
|  | {% block details %} | ||||||
|  |  | ||||||
|  | {% include 'order/tabs.html' with tab='attachments' %} | ||||||
|  |  | ||||||
|  | <h4>{% trans "Purchase Order Attachments" %} | ||||||
|  |  | ||||||
|  | <hr> | ||||||
|  |  | ||||||
|  | <div id='attachment-buttons'> | ||||||
|  |     <div class='btn-group'> | ||||||
|  |         <button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'> | ||||||
|  |     <thead> | ||||||
|  |         <tr> | ||||||
|  |             <th data-field='file' data-searchable='true'>{% trans "File" %}</th> | ||||||
|  |             <th data-field='comment' data-searchable='true'>{% trans "Comment" %}</th> | ||||||
|  |             <th></th> | ||||||
|  |         </tr> | ||||||
|  |     </thead> | ||||||
|  |     <tbody> | ||||||
|  |         {% for attachment in order.attachments.all %} | ||||||
|  |         <tr> | ||||||
|  |             <td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td> | ||||||
|  |             <td>{{ attachment.comment }}</td> | ||||||
|  |             <td> | ||||||
|  |                 <div class='btn-group' style='float: right;'>     | ||||||
|  |                     <button type='button' class='btn btn-default btn-glyph attachment-edit-button' url="{% url 'po-attachment-edit' attachment.id %}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'> | ||||||
|  |                         <span class='glyphicon glyphicon-edit'/> | ||||||
|  |                     </button> | ||||||
|  |                     <button type='button' class='btn btn-default btn-glyph attachment-delete-button' url="{% url 'po-attachment-delete' attachment.id %}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'> | ||||||
|  |                         <span class='glyphicon glyphicon-trash'/> | ||||||
|  |                     </button> | ||||||
|  |                 </div> | ||||||
|  |             </td> | ||||||
|  |         </tr> | ||||||
|  |         {% endfor %} | ||||||
|  |     </tbody> | ||||||
|  | </table> | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block js_ready %} | ||||||
|  | {{ block.super }} | ||||||
|  |  | ||||||
|  | $("#new-attachment").click(function() { | ||||||
|  |     launchModalForm("{% url 'po-attachment-create' %}?order={{ order.id }}", | ||||||
|  |         { | ||||||
|  |             reload: true, | ||||||
|  |         } | ||||||
|  |     ); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $("#attachment-table").on('click', '.attachment-edit-button', function() { | ||||||
|  |     var button = $(this); | ||||||
|  |  | ||||||
|  |     launchModalForm(button.attr('url'), { | ||||||
|  |         reload: true, | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $("#attachment-table").on('click', '.attachment-delete-button', function() { | ||||||
|  |     var button = $(this); | ||||||
|  |  | ||||||
|  |     launchModalForm(button.attr('url'), { | ||||||
|  |         reload: true, | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $("#attachment-table").inventreeTable({ | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										7
									
								
								InvenTree/order/templates/order/po_delete.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								InvenTree/order/templates/order/po_delete.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | {% extends "modal_delete_form.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block pre_form_content %} | ||||||
|  | {% trans "Are you sure you want to delete this attachment?" %} | ||||||
|  | <br> | ||||||
|  | {% endblock %} | ||||||
| @@ -12,7 +12,7 @@ | |||||||
|     {% for order in orders %} |     {% for order in orders %} | ||||||
|     <tr> |     <tr> | ||||||
|         <td>{% include "hover_image.html" with image=order.supplier.image hover=True %}<a href="{{ order.supplier.get_absolute_url }}purchase-orders/">{{ order.supplier.name }}</a></td> |         <td>{% include "hover_image.html" with image=order.supplier.image hover=True %}<a href="{{ order.supplier.get_absolute_url }}purchase-orders/">{{ order.supplier.name }}</a></td> | ||||||
|         <td><a href="{% url 'purchase-order-detail' order.id %}">{{ order }}</a></td> |         <td><a href="{% url 'po-detail' order.id %}">{{ order }}</a></td> | ||||||
|         <td>{{ order.description }}</td> |         <td>{{ order.description }}</td> | ||||||
|         <td>{% include "order/order_status.html" %}</td> |         <td>{% include "order/order_status.html" %}</td> | ||||||
|         <td>{{ order.lines.count }}</td> |         <td>{{ order.lines.count }}</td> | ||||||
|   | |||||||
| @@ -92,7 +92,7 @@ $("#po-lines-table").on('click', ".line-receive", function() { | |||||||
|  |  | ||||||
|     console.log('clicked! ' + button.attr('pk')); |     console.log('clicked! ' + button.attr('pk')); | ||||||
|  |  | ||||||
|     launchModalForm("{% url 'purchase-order-receive' order.id %}", { |     launchModalForm("{% url 'po-receive' order.id %}", { | ||||||
|         reload: true, |         reload: true, | ||||||
|         data: { |         data: { | ||||||
|             line: button.attr('pk') |             line: button.attr('pk') | ||||||
| @@ -109,7 +109,7 @@ $("#po-lines-table").on('click', ".line-receive", function() { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| $("#receive-order").click(function() { | $("#receive-order").click(function() { | ||||||
|     launchModalForm("{% url 'purchase-order-receive' order.id %}", { |     launchModalForm("{% url 'po-receive' order.id %}", { | ||||||
|         reload: true, |         reload: true, | ||||||
|         secondary: [ |         secondary: [ | ||||||
|             { |             { | ||||||
| @@ -123,13 +123,13 @@ $("#receive-order").click(function() { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| $("#complete-order").click(function() { | $("#complete-order").click(function() { | ||||||
|     launchModalForm("{% url 'purchase-order-complete' order.id %}", { |     launchModalForm("{% url 'po-complete' order.id %}", { | ||||||
|         reload: true, |         reload: true, | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $("#export-order").click(function() { | $("#export-order").click(function() { | ||||||
|     location.href = "{% url 'purchase-order-export' order.id %}"; |     location.href = "{% url 'po-export' order.id %}"; | ||||||
| }); | }); | ||||||
|  |  | ||||||
| {% if order.status == OrderStatus.PENDING %} | {% if order.status == OrderStatus.PENDING %} | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ InvenTree | Purchase Orders | |||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <table class='table table-striped table-condensed po-table' id='purchase-order-table'> | <table class='table table-striped table-condensed po-table' data-toolbar='#table-buttons' id='purchase-order-table'> | ||||||
| </table> | </table> | ||||||
|  |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| @@ -27,7 +27,7 @@ InvenTree | Purchase Orders | |||||||
| {{ block.super }} | {{ block.super }} | ||||||
|  |  | ||||||
| $("#po-create").click(function() { | $("#po-create").click(function() { | ||||||
|     launchModalForm("{% url 'purchase-order-create' %}", |     launchModalForm("{% url 'po-create' %}", | ||||||
|         { |         { | ||||||
|             follow: true, |             follow: true, | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -2,9 +2,16 @@ | |||||||
|  |  | ||||||
| <ul class='nav nav-tabs'> | <ul class='nav nav-tabs'> | ||||||
|     <li{% ifequal tab 'details' %} class='active'{% endifequal %}> |     <li{% ifequal tab 'details' %} class='active'{% endifequal %}> | ||||||
|         <a href="{% url 'purchase-order-detail' order.id %}">{% trans "Items" %}</a> |         <a href="{% url 'po-detail' order.id %}">{% trans "Items" %}</a> | ||||||
|  |     </li> | ||||||
|  |     <li{% if tab == 'attachments' %} class='active'{% endif %}> | ||||||
|  |         <a href="{% url 'po-attachments' order.id %}">{% trans "Attachments" %} | ||||||
|  |             {% if order.attachments.count > 0 %} | ||||||
|  |             <span class='badge'>{{ order.attachments.count }}</span> | ||||||
|  |             {% endif %} | ||||||
|  |         </a> | ||||||
|     </li> |     </li> | ||||||
|     <li{% ifequal tab 'notes' %} class='active'{% endifequal %}> |     <li{% ifequal tab 'notes' %} class='active'{% endifequal %}> | ||||||
|         <a href="{% url 'purchase-order-notes' order.id %}">{% trans "Notes" %}{% if order.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a> |         <a href="{% url 'po-notes' order.id %}">{% trans "Notes" %}{% if order.notes %} <span class='glyphicon glyphicon-small glyphicon-info-sign'></span>{% endif %}</a> | ||||||
|     </li> |     </li> | ||||||
| </ul> | </ul> | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ class OrderViewTestCase(TestCase): | |||||||
| class OrderListTest(OrderViewTestCase): | class OrderListTest(OrderViewTestCase): | ||||||
|  |  | ||||||
|     def test_order_list(self): |     def test_order_list(self): | ||||||
|         response = self.client.get(reverse('purchase-order-index')) |         response = self.client.get(reverse('po-index')) | ||||||
|  |  | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
| @@ -50,14 +50,14 @@ class POTests(OrderViewTestCase): | |||||||
|  |  | ||||||
|     def test_detail_view(self): |     def test_detail_view(self): | ||||||
|         """ Retrieve PO detail view """ |         """ Retrieve PO detail view """ | ||||||
|         response = self.client.get(reverse('purchase-order-detail', args=(1,))) |         response = self.client.get(reverse('po-detail', args=(1,))) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         keys = response.context.keys() |         keys = response.context.keys() | ||||||
|         self.assertIn('OrderStatus', keys) |         self.assertIn('OrderStatus', keys) | ||||||
|  |  | ||||||
|     def test_po_create(self): |     def test_po_create(self): | ||||||
|         """ Launch forms to create new PurchaseOrder""" |         """ Launch forms to create new PurchaseOrder""" | ||||||
|         url = reverse('purchase-order-create') |         url = reverse('po-create') | ||||||
|  |  | ||||||
|         # Without a supplier ID |         # Without a supplier ID | ||||||
|         response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') |         response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||||
| @@ -74,13 +74,13 @@ class POTests(OrderViewTestCase): | |||||||
|     def test_po_edit(self): |     def test_po_edit(self): | ||||||
|         """ Launch form to edit a PurchaseOrder """ |         """ Launch form to edit a PurchaseOrder """ | ||||||
|  |  | ||||||
|         response = self.client.get(reverse('purchase-order-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') |         response = self.client.get(reverse('po-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|     def test_po_export(self): |     def test_po_export(self): | ||||||
|         """ Export PurchaseOrder """ |         """ Export PurchaseOrder """ | ||||||
|  |  | ||||||
|         response = self.client.get(reverse('purchase-order-export', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') |         response = self.client.get(reverse('po-export', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||||
|  |  | ||||||
|         # Response should be streaming-content (file download) |         # Response should be streaming-content (file download) | ||||||
|         self.assertIn('streaming_content', dir(response)) |         self.assertIn('streaming_content', dir(response)) | ||||||
| @@ -88,7 +88,7 @@ class POTests(OrderViewTestCase): | |||||||
|     def test_po_issue(self): |     def test_po_issue(self): | ||||||
|         """ Test PurchaseOrderIssue view """ |         """ Test PurchaseOrderIssue view """ | ||||||
|  |  | ||||||
|         url = reverse('purchase-order-issue', args=(1,)) |         url = reverse('po-issue', args=(1,)) | ||||||
|  |  | ||||||
|         order = PurchaseOrder.objects.get(pk=1) |         order = PurchaseOrder.objects.get(pk=1) | ||||||
|         self.assertEqual(order.status, OrderStatus.PENDING) |         self.assertEqual(order.status, OrderStatus.PENDING) | ||||||
| @@ -183,7 +183,7 @@ class TestPOReceive(OrderViewTestCase): | |||||||
|         self.po = PurchaseOrder.objects.get(pk=1) |         self.po = PurchaseOrder.objects.get(pk=1) | ||||||
|         self.po.status = OrderStatus.PLACED |         self.po.status = OrderStatus.PLACED | ||||||
|         self.po.save() |         self.po.save() | ||||||
|         self.url = reverse('purchase-order-receive', args=(1,)) |         self.url = reverse('po-receive', args=(1,)) | ||||||
|  |  | ||||||
|     def post(self, data, validate=None): |     def post(self, data, validate=None): | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,19 +9,26 @@ from django.conf.urls import url, include | |||||||
|  |  | ||||||
| from . import views | from . import views | ||||||
|  |  | ||||||
|  | purchase_order_attachment_urls = [ | ||||||
|  |     url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'), | ||||||
|  |     url(r'^(?P<pk>\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'), | ||||||
|  |     url(r'^(?P<pk>\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'), | ||||||
|  | ] | ||||||
|  |  | ||||||
| purchase_order_detail_urls = [ | purchase_order_detail_urls = [ | ||||||
|  |  | ||||||
|     url(r'^cancel/?', views.PurchaseOrderCancel.as_view(), name='purchase-order-cancel'), |     url(r'^cancel/?', views.PurchaseOrderCancel.as_view(), name='po-cancel'), | ||||||
|     url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='purchase-order-edit'), |     url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='po-edit'), | ||||||
|     url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='purchase-order-issue'), |     url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='po-issue'), | ||||||
|     url(r'^receive/?', views.PurchaseOrderReceive.as_view(), name='purchase-order-receive'), |     url(r'^receive/?', views.PurchaseOrderReceive.as_view(), name='po-receive'), | ||||||
|     url(r'^complete/?', views.PurchaseOrderComplete.as_view(), name='purchase-order-complete'), |     url(r'^complete/?', views.PurchaseOrderComplete.as_view(), name='po-complete'), | ||||||
|  |  | ||||||
|     url(r'^export/?', views.PurchaseOrderExport.as_view(), name='purchase-order-export'), |     url(r'^export/?', views.PurchaseOrderExport.as_view(), name='po-export'), | ||||||
|  |  | ||||||
|     url(r'^notes/', views.PurchaseOrderNotes.as_view(), name='purchase-order-notes'), |     url(r'^notes/', views.PurchaseOrderNotes.as_view(), name='po-notes'), | ||||||
|  |  | ||||||
|     url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'), |     url(r'^attachments/', views.PurchaseOrderDetail.as_view(template_name='order/po_attachments.html'), name='po-attachments'), | ||||||
|  |     url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='po-detail'), | ||||||
| ] | ] | ||||||
|  |  | ||||||
| po_line_item_detail_urls = [ | po_line_item_detail_urls = [ | ||||||
| @@ -39,7 +46,7 @@ po_line_urls = [ | |||||||
|  |  | ||||||
| purchase_order_urls = [ | purchase_order_urls = [ | ||||||
|  |  | ||||||
|     url(r'^new/', views.PurchaseOrderCreate.as_view(), name='purchase-order-create'), |     url(r'^new/', views.PurchaseOrderCreate.as_view(), name='po-create'), | ||||||
|  |  | ||||||
|     url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'), |     url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'), | ||||||
|  |  | ||||||
| @@ -48,8 +55,10 @@ purchase_order_urls = [ | |||||||
|  |  | ||||||
|     url(r'^line/', include(po_line_urls)), |     url(r'^line/', include(po_line_urls)), | ||||||
|  |  | ||||||
|  |     url(r'^attachments/', include(purchase_order_attachment_urls)), | ||||||
|  |  | ||||||
|     # Display complete list of purchase orders |     # Display complete list of purchase orders | ||||||
|     url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='purchase-order-index'), |     url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'), | ||||||
| ] | ] | ||||||
|  |  | ||||||
| order_urls = [ | order_urls = [ | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ from django.forms import HiddenInput | |||||||
| import logging | import logging | ||||||
| from decimal import Decimal, InvalidOperation | from decimal import Decimal, InvalidOperation | ||||||
|  |  | ||||||
| from .models import PurchaseOrder, PurchaseOrderLineItem | from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment | ||||||
| from .admin import POLineItemResource | from .admin import POLineItemResource | ||||||
| from build.models import Build | from build.models import Build | ||||||
| from company.models import Company, SupplierPart | from company.models import Company, SupplierPart | ||||||
| @@ -70,6 +70,84 @@ class PurchaseOrderDetail(DetailView): | |||||||
|         return ctx |         return ctx | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PurchaseOrderAttachmentCreate(AjaxCreateView): | ||||||
|  |     """ | ||||||
|  |     View for creating a new PurchaseOrderAtt | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     model = PurchaseOrderAttachment | ||||||
|  |     form_class = order_forms.EditPurchaseOrderAttachmentForm | ||||||
|  |     ajax_form_title = _("Add Purchase Order Attachment") | ||||||
|  |     ajax_template_name = "modal_form.html" | ||||||
|  |  | ||||||
|  |     def get_data(self): | ||||||
|  |         return { | ||||||
|  |             "success": _("Added attachment") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def get_initial(self): | ||||||
|  |         """ | ||||||
|  |         Get initial data for creating a new PurchaseOrderAttachment object. | ||||||
|  |  | ||||||
|  |         - Client must request this form with a parent PurchaseOrder in midn. | ||||||
|  |         - e.g. ?order=<pk> | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         initials = super(AjaxCreateView, self).get_initial() | ||||||
|  |  | ||||||
|  |         initials["order"] = PurchaseOrder.objects.get(id=self.request.GET.get('order', -1)) | ||||||
|  |  | ||||||
|  |         return initials | ||||||
|  |  | ||||||
|  |     def get_form(self): | ||||||
|  |         """ | ||||||
|  |         Create a form to upload a new PurchaseOrderAttachment | ||||||
|  |  | ||||||
|  |         - Hide the 'order' field | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         form = super(AjaxCreateView, self).get_form() | ||||||
|  |  | ||||||
|  |         form.fields['order'].widget = HiddenInput() | ||||||
|  |  | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PurchaseOrderAttachmentEdit(AjaxUpdateView): | ||||||
|  |     """ View for editing a PurchaseOrderAttachment object """ | ||||||
|  |  | ||||||
|  |     model = PurchaseOrderAttachment | ||||||
|  |     form_class = order_forms.EditPurchaseOrderAttachmentForm | ||||||
|  |     ajax_form_title = _("Edit Attachment") | ||||||
|  |  | ||||||
|  |     def get_data(self): | ||||||
|  |         return { | ||||||
|  |             'success': _('Attachment updated') | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def get_form(self): | ||||||
|  |         form = super(AjaxUpdateView, self).get_form() | ||||||
|  |  | ||||||
|  |         # Hide the 'order' field | ||||||
|  |         form.fields['order'].widget = HiddenInput() | ||||||
|  |  | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PurchaseOrderAttachmentDelete(AjaxDeleteView): | ||||||
|  |     """ View for deleting a PurchaseOrderAttachment """ | ||||||
|  |  | ||||||
|  |     model = PurchaseOrderAttachment | ||||||
|  |     ajax_form_title = _("Delete Attachment") | ||||||
|  |     ajax_template_name = "order/po_delete.html" | ||||||
|  |     context_object_name = "attachment" | ||||||
|  |  | ||||||
|  |     def get_data(self): | ||||||
|  |         return { | ||||||
|  |             "danger": _("Deleted attachment") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class PurchaseOrderNotes(UpdateView): | class PurchaseOrderNotes(UpdateView): | ||||||
|     """ View for updating the 'notes' field of a PurchaseOrder """ |     """ View for updating the 'notes' field of a PurchaseOrder """ | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								InvenTree/part/migrations/0032_auto_20200322_0453.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								InvenTree/part/migrations/0032_auto_20200322_0453.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | # Generated by Django 2.2.9 on 2020-03-22 04:53 | ||||||
|  |  | ||||||
|  | import InvenTree.models | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('part', '0031_auto_20200318_1044'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='partattachment', | ||||||
|  |             name='attachment', | ||||||
|  |             field=models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -34,7 +34,7 @@ import hashlib | |||||||
|  |  | ||||||
| from InvenTree import helpers | from InvenTree import helpers | ||||||
| from InvenTree import validators | from InvenTree import validators | ||||||
| from InvenTree.models import InvenTreeTree | from InvenTree.models import InvenTreeTree, InvenTreeAttachment | ||||||
| from InvenTree.fields import InvenTreeURLField | from InvenTree.fields import InvenTreeURLField | ||||||
| from InvenTree.helpers import decimal2string | from InvenTree.helpers import decimal2string | ||||||
|  |  | ||||||
| @@ -941,28 +941,17 @@ def attach_file(instance, filename): | |||||||
|     return os.path.join('part_files', str(instance.part.id), filename) |     return os.path.join('part_files', str(instance.part.id), filename) | ||||||
|  |  | ||||||
|  |  | ||||||
| class PartAttachment(models.Model): | class PartAttachment(InvenTreeAttachment): | ||||||
|     """ A PartAttachment links a file to a part |  | ||||||
|     Parts can have multiple files such as datasheets, etc |  | ||||||
|  |  | ||||||
|     Attributes: |  | ||||||
|         part: Link to a Part object |  | ||||||
|         attachment: File |  | ||||||
|         comment: String descriptor for the attachment |  | ||||||
|     """ |     """ | ||||||
|  |     Model for storing file attachments against a Part object | ||||||
|  |     """ | ||||||
|  |      | ||||||
|  |     def getSubdir(self): | ||||||
|  |         return os.path.join("part_files", str(self.part.id)) | ||||||
|  |  | ||||||
|     part = models.ForeignKey(Part, on_delete=models.CASCADE, |     part = models.ForeignKey(Part, on_delete=models.CASCADE, | ||||||
|                              related_name='attachments') |                              related_name='attachments') | ||||||
|  |  | ||||||
|     attachment = models.FileField(upload_to=attach_file, |  | ||||||
|                                   help_text=_('Select file to attach')) |  | ||||||
|  |  | ||||||
|     comment = models.CharField(max_length=100, help_text=_('File comment')) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def basename(self): |  | ||||||
|         return os.path.basename(self.attachment.name) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PartStar(models.Model): | class PartStar(models.Model): | ||||||
|     """ A PartStar object creates a relationship between a User and a Part. |     """ A PartStar object creates a relationship between a User and a Part. | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| {% extends "modal_delete_form.html" %} | {% extends "modal_delete_form.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
| {% block pre_form_content %} | {% block pre_form_content %} | ||||||
| Are you sure you wish to delete this attachment? | {% trans "Are you sure you want to delete this attachment?" %} | ||||||
| <br> | <br> | ||||||
| This will remove the file '{{ attachment.basename }}'. |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| @@ -13,20 +13,20 @@ from django.conf.urls import url, include | |||||||
| from . import views | from . import views | ||||||
|  |  | ||||||
| part_attachment_urls = [ | part_attachment_urls = [ | ||||||
|     url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), |     url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), | ||||||
|     url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), |     url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), | ||||||
|     url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), |     url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), | ||||||
| ] | ] | ||||||
|  |  | ||||||
| part_parameter_urls = [ | part_parameter_urls = [ | ||||||
|      |      | ||||||
|     url('^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), |     url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), | ||||||
|     url('^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'), |     url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'), | ||||||
|     url('^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), |     url(r'^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), | ||||||
|      |      | ||||||
|     url('^new/', views.PartParameterCreate.as_view(), name='part-param-create'), |     url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), | ||||||
|     url('^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), |     url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), | ||||||
|     url('^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), |     url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), | ||||||
|  |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -112,6 +112,7 @@ class PartAttachmentCreate(AjaxCreateView): | |||||||
|  |  | ||||||
| class PartAttachmentEdit(AjaxUpdateView): | class PartAttachmentEdit(AjaxUpdateView): | ||||||
|     """ View for editing a PartAttachment object """ |     """ View for editing a PartAttachment object """ | ||||||
|  |      | ||||||
|     model = PartAttachment |     model = PartAttachment | ||||||
|     form_class = part_forms.EditPartAttachmentForm |     form_class = part_forms.EditPartAttachmentForm | ||||||
|     ajax_template_name = 'modal_form.html' |     ajax_template_name = 'modal_form.html' | ||||||
|   | |||||||
| @@ -117,7 +117,7 @@ | |||||||
|             {% if item.purchase_order %} |             {% if item.purchase_order %} | ||||||
|             <tr> |             <tr> | ||||||
|                 <td>{% trans "Purchase Order" %}</td> |                 <td>{% trans "Purchase Order" %}</td> | ||||||
|                 <td><a href="{% url 'purchase-order-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td> |                 <td><a href="{% url 'po-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td> | ||||||
|             </tr> |             </tr> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|             {% if item.customer %} |             {% if item.customer %} | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|       <li><a href="{% url 'stock-index' %}">{% trans "Stock" %}</a></li> |       <li><a href="{% url 'stock-index' %}">{% trans "Stock" %}</a></li> | ||||||
|       <li><a href="{% url 'build-index' %}">{% trans "Build" %}</a></li> |       <li><a href="{% url 'build-index' %}">{% trans "Build" %}</a></li> | ||||||
|       <li><a href="{% url 'company-index' %}">{% trans "Suppliers" %}</a></li> |       <li><a href="{% url 'company-index' %}">{% trans "Suppliers" %}</a></li> | ||||||
|       <li><a href="{% url 'purchase-order-index' %}">{% trans "Orders" %}</a></li> |       <li><a href="{% url 'po-index' %}">{% trans "Orders" %}</a></li> | ||||||
|     </ul> |     </ul> | ||||||
|     <ul class="nav navbar-nav navbar-right"> |     <ul class="nav navbar-nav navbar-right"> | ||||||
|         {% include "search_form.html" %} |         {% include "search_form.html" %} | ||||||
|   | |||||||
| @@ -21,6 +21,6 @@ For site administrator and project code documentation, refer to the [developer d | |||||||
|  |  | ||||||
| Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start.html) for installation and setup instructions. | Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start.html) for installation and setup instructions. | ||||||
|  |  | ||||||
| ### Third Party | ## Third Party Extensions | ||||||
|  |  | ||||||
| [InvenTree Docker](https://github.com/Zeigren/inventree-docker) - A docker build for InvenTree by [Zeigren](https://github.com/Zeigren) | [InvenTree Docker](https://github.com/Zeigren/inventree-docker) - A docker build for InvenTree by [Zeigren](https://github.com/Zeigren) | ||||||
|   | |||||||
| @@ -6,5 +6,5 @@ ignore = | |||||||
| 	E501, E722, | 	E501, E722, | ||||||
| 	# - C901 - function is too complex | 	# - C901 - function is too complex | ||||||
| 	C901, | 	C901, | ||||||
| exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/* | exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/* | ||||||
| max-complexity = 20 | max-complexity = 20 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user