mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05: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 | ||||
|  | ||||
| import os | ||||
|  | ||||
| from django.db import models | ||||
| 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.dispatch import receiver | ||||
| @@ -15,6 +18,51 @@ from mptt.models import MPTTModel, TreeForeignKey | ||||
| 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): | ||||
|     """ Provides an abstracted self-referencing tree model for data categories. | ||||
|  | ||||
|   | ||||
| @@ -100,7 +100,7 @@ InvenTree | {% trans "Company" %} - {{ company.name }} | ||||
|     }); | ||||
|  | ||||
|     $("#company-order-2").click(function() { | ||||
|         launchModalForm("{% url 'purchase-order-create' %}", | ||||
|         launchModalForm("{% url 'po-create' %}", | ||||
|         { | ||||
|             data: { | ||||
|                 supplier: {{ company.id }}, | ||||
|   | ||||
| @@ -28,7 +28,7 @@ | ||||
|  | ||||
|  | ||||
|     function newOrder() { | ||||
|         launchModalForm("{% url 'purchase-order-create' %}", | ||||
|         launchModalForm("{% url 'po-create' %}", | ||||
|         { | ||||
|             data: { | ||||
|                 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 stock.models import StockLocation | ||||
| from .models import PurchaseOrder, PurchaseOrderLineItem | ||||
| from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     """ 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 | ||||
|  | ||||
| import os | ||||
| from datetime import datetime | ||||
|  | ||||
| from stock.models import StockItem | ||||
| @@ -21,6 +22,7 @@ from company.models import Company, SupplierPart | ||||
|  | ||||
| from InvenTree.helpers import decimal2string | ||||
| from InvenTree.status_codes import OrderStatus | ||||
| from InvenTree.models import InvenTreeAttachment | ||||
|  | ||||
|  | ||||
| class Order(models.Model): | ||||
| @@ -136,7 +138,7 @@ class PurchaseOrder(Order): | ||||
|     ) | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse('purchase-order-detail', kwargs={'pk': self.id}) | ||||
|         return reverse('po-detail', kwargs={'pk': self.id}) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     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 | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     """ Abstract model for an order line item | ||||
|      | ||||
|   | ||||
| @@ -107,7 +107,7 @@ InvenTree | {{ order }} | ||||
|  | ||||
| {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} | ||||
| $("#place-order").click(function() { | ||||
|     launchModalForm("{% url 'purchase-order-issue' order.id %}", | ||||
|     launchModalForm("{% url 'po-issue' order.id %}", | ||||
|     { | ||||
|         reload: true, | ||||
|     }); | ||||
| @@ -115,7 +115,7 @@ $("#place-order").click(function() { | ||||
| {% endif %} | ||||
|  | ||||
| $("#edit-order").click(function() { | ||||
|     launchModalForm("{% url 'purchase-order-edit' order.id %}", | ||||
|     launchModalForm("{% url 'po-edit' order.id %}", | ||||
|         { | ||||
|             reload: true, | ||||
|         } | ||||
| @@ -123,7 +123,7 @@ $("#edit-order").click(function() { | ||||
| }); | ||||
|  | ||||
| $("#cancel-order").click(function() { | ||||
|     launchModalForm("{% url 'purchase-order-cancel' order.id %}", { | ||||
|     launchModalForm("{% url 'po-cancel' order.id %}", { | ||||
|         reload: true, | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -50,7 +50,7 @@ | ||||
| {% if editing %} | ||||
| {% else %} | ||||
| $("#edit-notes").click(function() { | ||||
|     location.href = "{% url 'purchase-order-notes' order.id %}?edit=1"; | ||||
|     location.href = "{% url 'po-notes' order.id %}?edit=1"; | ||||
| }); | ||||
| {% 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 %} | ||||
|     <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><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>{% include "order/order_status.html" %}</td> | ||||
|         <td>{{ order.lines.count }}</td> | ||||
|   | ||||
| @@ -92,7 +92,7 @@ $("#po-lines-table").on('click', ".line-receive", function() { | ||||
|  | ||||
|     console.log('clicked! ' + button.attr('pk')); | ||||
|  | ||||
|     launchModalForm("{% url 'purchase-order-receive' order.id %}", { | ||||
|     launchModalForm("{% url 'po-receive' order.id %}", { | ||||
|         reload: true, | ||||
|         data: { | ||||
|             line: button.attr('pk') | ||||
| @@ -109,7 +109,7 @@ $("#po-lines-table").on('click', ".line-receive", function() { | ||||
| }); | ||||
|  | ||||
| $("#receive-order").click(function() { | ||||
|     launchModalForm("{% url 'purchase-order-receive' order.id %}", { | ||||
|     launchModalForm("{% url 'po-receive' order.id %}", { | ||||
|         reload: true, | ||||
|         secondary: [ | ||||
|             { | ||||
| @@ -123,13 +123,13 @@ $("#receive-order").click(function() { | ||||
| }); | ||||
|  | ||||
| $("#complete-order").click(function() { | ||||
|     launchModalForm("{% url 'purchase-order-complete' order.id %}", { | ||||
|     launchModalForm("{% url 'po-complete' order.id %}", { | ||||
|         reload: true, | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| $("#export-order").click(function() { | ||||
|     location.href = "{% url 'purchase-order-export' order.id %}"; | ||||
|     location.href = "{% url 'po-export' order.id %}"; | ||||
| }); | ||||
|  | ||||
| {% if order.status == OrderStatus.PENDING %} | ||||
|   | ||||
| @@ -18,7 +18,7 @@ InvenTree | Purchase Orders | ||||
|     </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> | ||||
|  | ||||
| {% endblock %} | ||||
| @@ -27,7 +27,7 @@ InvenTree | Purchase Orders | ||||
| {{ block.super }} | ||||
|  | ||||
| $("#po-create").click(function() { | ||||
|     launchModalForm("{% url 'purchase-order-create' %}", | ||||
|     launchModalForm("{% url 'po-create' %}", | ||||
|         { | ||||
|             follow: true, | ||||
|         } | ||||
|   | ||||
| @@ -2,9 +2,16 @@ | ||||
|  | ||||
| <ul class='nav nav-tabs'> | ||||
|     <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{% 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> | ||||
| </ul> | ||||
|   | ||||
| @@ -40,7 +40,7 @@ class OrderViewTestCase(TestCase): | ||||
| class OrderListTest(OrderViewTestCase): | ||||
|  | ||||
|     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) | ||||
|  | ||||
| @@ -50,14 +50,14 @@ class POTests(OrderViewTestCase): | ||||
|  | ||||
|     def test_detail_view(self): | ||||
|         """ 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) | ||||
|         keys = response.context.keys() | ||||
|         self.assertIn('OrderStatus', keys) | ||||
|  | ||||
|     def test_po_create(self): | ||||
|         """ Launch forms to create new PurchaseOrder""" | ||||
|         url = reverse('purchase-order-create') | ||||
|         url = reverse('po-create') | ||||
|  | ||||
|         # Without a supplier ID | ||||
|         response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') | ||||
| @@ -74,13 +74,13 @@ class POTests(OrderViewTestCase): | ||||
|     def test_po_edit(self): | ||||
|         """ 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) | ||||
|  | ||||
|     def test_po_export(self): | ||||
|         """ 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) | ||||
|         self.assertIn('streaming_content', dir(response)) | ||||
| @@ -88,7 +88,7 @@ class POTests(OrderViewTestCase): | ||||
|     def test_po_issue(self): | ||||
|         """ Test PurchaseOrderIssue view """ | ||||
|  | ||||
|         url = reverse('purchase-order-issue', args=(1,)) | ||||
|         url = reverse('po-issue', args=(1,)) | ||||
|  | ||||
|         order = PurchaseOrder.objects.get(pk=1) | ||||
|         self.assertEqual(order.status, OrderStatus.PENDING) | ||||
| @@ -183,7 +183,7 @@ class TestPOReceive(OrderViewTestCase): | ||||
|         self.po = PurchaseOrder.objects.get(pk=1) | ||||
|         self.po.status = OrderStatus.PLACED | ||||
|         self.po.save() | ||||
|         self.url = reverse('purchase-order-receive', args=(1,)) | ||||
|         self.url = reverse('po-receive', args=(1,)) | ||||
|  | ||||
|     def post(self, data, validate=None): | ||||
|  | ||||
|   | ||||
| @@ -9,19 +9,26 @@ from django.conf.urls import url, include | ||||
|  | ||||
| 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 = [ | ||||
|  | ||||
|     url(r'^cancel/?', views.PurchaseOrderCancel.as_view(), name='purchase-order-cancel'), | ||||
|     url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='purchase-order-edit'), | ||||
|     url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='purchase-order-issue'), | ||||
|     url(r'^receive/?', views.PurchaseOrderReceive.as_view(), name='purchase-order-receive'), | ||||
|     url(r'^complete/?', views.PurchaseOrderComplete.as_view(), name='purchase-order-complete'), | ||||
|     url(r'^cancel/?', views.PurchaseOrderCancel.as_view(), name='po-cancel'), | ||||
|     url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='po-edit'), | ||||
|     url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='po-issue'), | ||||
|     url(r'^receive/?', views.PurchaseOrderReceive.as_view(), name='po-receive'), | ||||
|     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 = [ | ||||
| @@ -39,7 +46,7 @@ po_line_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'), | ||||
|  | ||||
| @@ -48,8 +55,10 @@ purchase_order_urls = [ | ||||
|  | ||||
|     url(r'^line/', include(po_line_urls)), | ||||
|  | ||||
|     url(r'^attachments/', include(purchase_order_attachment_urls)), | ||||
|  | ||||
|     # 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 = [ | ||||
|   | ||||
| @@ -15,7 +15,7 @@ from django.forms import HiddenInput | ||||
| import logging | ||||
| from decimal import Decimal, InvalidOperation | ||||
|  | ||||
| from .models import PurchaseOrder, PurchaseOrderLineItem | ||||
| from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment | ||||
| from .admin import POLineItemResource | ||||
| from build.models import Build | ||||
| from company.models import Company, SupplierPart | ||||
| @@ -70,6 +70,84 @@ class PurchaseOrderDetail(DetailView): | ||||
|         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): | ||||
|     """ 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 validators | ||||
| from InvenTree.models import InvenTreeTree | ||||
| from InvenTree.models import InvenTreeTree, InvenTreeAttachment | ||||
| from InvenTree.fields import InvenTreeURLField | ||||
| 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) | ||||
|  | ||||
|  | ||||
| class PartAttachment(models.Model): | ||||
|     """ 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 | ||||
| class PartAttachment(InvenTreeAttachment): | ||||
|     """ | ||||
|     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, | ||||
|                              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): | ||||
|     """ A PartStar object creates a relationship between a User and a Part. | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| {% extends "modal_delete_form.html" %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block pre_form_content %} | ||||
| Are you sure you wish to delete this attachment? | ||||
| {% trans "Are you sure you want to delete this attachment?" %} | ||||
| <br> | ||||
| This will remove the file '{{ attachment.basename }}'. | ||||
| {% endblock %} | ||||
| @@ -13,20 +13,20 @@ from django.conf.urls import url, include | ||||
| from . import views | ||||
|  | ||||
| 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+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), | ||||
| ] | ||||
|  | ||||
| part_parameter_urls = [ | ||||
|      | ||||
|     url('^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('^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), | ||||
|     url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), | ||||
|     url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.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('^(?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'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), | ||||
|     url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), | ||||
|     url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), | ||||
|  | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -112,6 +112,7 @@ class PartAttachmentCreate(AjaxCreateView): | ||||
|  | ||||
| class PartAttachmentEdit(AjaxUpdateView): | ||||
|     """ View for editing a PartAttachment object """ | ||||
|      | ||||
|     model = PartAttachment | ||||
|     form_class = part_forms.EditPartAttachmentForm | ||||
|     ajax_template_name = 'modal_form.html' | ||||
|   | ||||
| @@ -117,7 +117,7 @@ | ||||
|             {% if item.purchase_order %} | ||||
|             <tr> | ||||
|                 <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> | ||||
|             {% endif %} | ||||
|             {% if item.customer %} | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|       <li><a href="{% url 'stock-index' %}">{% trans "Stock" %}</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 'purchase-order-index' %}">{% trans "Orders" %}</a></li> | ||||
|       <li><a href="{% url 'po-index' %}">{% trans "Orders" %}</a></li> | ||||
|     </ul> | ||||
|     <ul class="nav navbar-nav navbar-right"> | ||||
|         {% 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. | ||||
|  | ||||
| ### Third Party | ||||
| ## Third Party Extensions | ||||
|  | ||||
| [InvenTree Docker](https://github.com/Zeigren/inventree-docker) - A docker build for InvenTree by [Zeigren](https://github.com/Zeigren) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user