Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-03-22 20:20:22 +11:00
commit ed6abcdf32
30 changed files with 1354 additions and 789 deletions

View File

@ -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.

View File

@ -4,7 +4,7 @@ Provides information on the current InvenTree version
import subprocess import subprocess
INVENTREE_SW_VERSION = "0.0.8" INVENTREE_SW_VERSION = "0.0.9"
def inventreeVersion(): def inventreeVersion():

View File

@ -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 }},

View File

@ -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

View File

@ -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 """

View 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,
},
),
]

View File

@ -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

View File

@ -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,
}); });
}); });

View File

@ -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 %}

View 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 %}

View 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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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,
} }

View File

@ -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>

View File

@ -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):

View File

@ -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 = [

View File

@ -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 """

View 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),
),
]

View File

@ -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.

View File

@ -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 %}

View File

@ -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'),
] ]

View File

@ -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'

View File

@ -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 %}

View File

@ -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" %}

View File

@ -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)

View File

@ -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