mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Merge remote-tracking branch 'inventree/master'
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.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ Provides information on the current InvenTree version
 | 
			
		||||
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
INVENTREE_SW_VERSION = "0.0.8"
 | 
			
		||||
INVENTREE_SW_VERSION = "0.0.9"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def inventreeVersion():
 | 
			
		||||
 
 | 
			
		||||
@@ -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