From be6b1ae2f845e08008375042097dbb62caeb4721 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 5 Jun 2019 20:59:30 +1000
Subject: [PATCH] Updates for purchase order line items

- Display list of line items
- Add a form to create a new line item
---
 InvenTree/order/forms.py                      |  23 ++++
 .../0005_purchaseorderlineitem_part.py        |  20 ++++
 .../migrations/0006_auto_20190605_2056.py     |  18 +++
 InvenTree/order/models.py                     |  23 +++-
 .../order/purchase_order_detail.html          | 105 +++++++++++++++++-
 .../templates/order/purchase_orders.html      |   4 +
 InvenTree/order/urls.py                       |   7 ++
 InvenTree/order/views.py                      |  82 +++++++++++++-
 InvenTree/static/css/inventree.css            |   4 +
 9 files changed, 274 insertions(+), 12 deletions(-)
 create mode 100644 InvenTree/order/forms.py
 create mode 100644 InvenTree/order/migrations/0005_purchaseorderlineitem_part.py
 create mode 100644 InvenTree/order/migrations/0006_auto_20190605_2056.py

diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py
new file mode 100644
index 0000000000..627b5f130e
--- /dev/null
+++ b/InvenTree/order/forms.py
@@ -0,0 +1,23 @@
+"""
+Django Forms for interacting with Order objects
+"""
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from InvenTree.forms import HelperForm
+
+from .models import PurchaseOrder, PurchaseOrderLineItem
+
+
+class EditPurchaseOrderLineItemForm(HelperForm):
+
+    class Meta:
+        model = PurchaseOrderLineItem
+        fields = [
+            'order',
+            'part',
+            'quantity',
+            'reference',
+            'received'
+        ]
\ No newline at end of file
diff --git a/InvenTree/order/migrations/0005_purchaseorderlineitem_part.py b/InvenTree/order/migrations/0005_purchaseorderlineitem_part.py
new file mode 100644
index 0000000000..61f0944156
--- /dev/null
+++ b/InvenTree/order/migrations/0005_purchaseorderlineitem_part.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2 on 2019-06-05 10:36
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('company', '0005_auto_20190525_2356'),
+        ('order', '0004_purchaseorder_status'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='purchaseorderlineitem',
+            name='part',
+            field=models.ForeignKey(blank=True, help_text='Supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='company.SupplierPart'),
+        ),
+    ]
diff --git a/InvenTree/order/migrations/0006_auto_20190605_2056.py b/InvenTree/order/migrations/0006_auto_20190605_2056.py
new file mode 100644
index 0000000000..e938343d01
--- /dev/null
+++ b/InvenTree/order/migrations/0006_auto_20190605_2056.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-06-05 10:56
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('company', '0005_auto_20190525_2356'),
+        ('order', '0005_purchaseorderlineitem_part'),
+    ]
+
+    operations = [
+        migrations.AlterUniqueTogether(
+            name='purchaseorderlineitem',
+            unique_together={('order', 'part')},
+        ),
+    ]
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index e9188dc869..56ef5952fb 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
 
 from django.utils.translation import ugettext as _
 
-from company.models import Company
+from company.models import Company, SupplierPart
 
 from InvenTree.status_codes import OrderStatus
 
@@ -108,11 +108,24 @@ class PurchaseOrderLineItem(OrderLineItem):
 
     """
 
-    order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE,
-                              related_name='lines',
-                              help_text=_('Purchase Order')
-                              )
+    class Meta:
+        unique_together = (
+            ('order', 'part')
+        )
+
+    order = models.ForeignKey(
+        PurchaseOrder, on_delete=models.CASCADE,
+        related_name='lines',
+        help_text=_('Purchase Order')
+    )
 
     # TODO - foreign key references to part and stockitem objects
 
+    part = models.ForeignKey(
+        SupplierPart, on_delete=models.SET_NULL,
+        blank=True, null=True,
+        related_name='orders',
+        help_text=_("Supplier part"),
+    )
+
     received = models.PositiveIntegerField(default=0, help_text=_('Number of items received'))
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index bea88a21ba..2d40de6459 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -1,9 +1,106 @@
 {% extends "base.html" %}
 
-{% block content %}
+{% load static %}
 
-Purchase Order: {{ order.reference }}
-<br><hr>
-Description: {{ order.description }}
+{% block page_title %}
+InvenTree | {{ order }}
+{% endblock %}
+
+{% block content %}
+<div class='row'>
+    <div class='col-sm-6'>
+        <div class='media'>
+            <div class='media-left'>
+                <img class='part-thumb'
+                {% if order.supplier.image %}
+                src="{{ order.supplier.image.url }}"
+                {% else %}
+                src="{% static 'img/blank_image.png' %}"
+                {% endif %}
+                />
+            </div>
+            <div class='media-body'>
+                <h4>{{ order }}</h4>
+                <p>{{ order.description }}</p>
+                {% if order.URL %}
+                <a href="{{ order.URL }}">{{ order.URL }}</a>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+    <div class='col-sm-6'>
+        <table class='table'>
+            <tr>
+                <td>Status</td>
+                <td>{% include "order/order_status.html" %}</td>
+            </tr>
+            <tr>
+                <td>Created</td>
+                <td>{{ order.creation_date }}</td>
+            </tr>
+            <tr>
+                <td>Created By</td>
+                <td>{{ order.created_by }}</td>
+            </tr>
+            {% if order.issue_date %}
+            <tr>
+                <td>Issued</td>
+                <td>{{ order.issue_date }}</td>
+            </tr>
+            {% endif %}
+        </table>
+    </div>
+</div>
+
+<hr>
+
+<h4>Order Items</h4>
+
+<button type='button' class='btn btn-default' id='new-po-line'>Add Line Item</button>
+
+<table class='table table-striped table-condensed' id='po-lines-table'>
+    <tr>
+        <th data-field='line'>Line</th>
+        <th data-field='part'>Part</th>
+        <th data-field='reference'>Reference</th>
+        <th data-field='quantity'>Quantity</th>
+        <th data-field='received'>Received</th>
+    </tr>
+    {% for line in order.lines.all %}
+    <tr>
+        <td>{{ forloop.counter }}</td>
+        <td></td>
+        <td>{{ line.reference }}</td>
+        <td>{{ line.quantity }}</td>
+        <td>{{ line.received }}</td>
+    </tr>
+    {% endfor %}
+</table>
+
+{% if order.notes %}
+<hr>
+<div class='panel panel-default'>
+    <div class='panel-heading'><b>Notes</b></div>
+    <div class='panel-body'>{{ order.notes }}</div>
+</div>
+{% endif %}
+
+{% endblock %}
+
+{% block js_ready %}
+
+$('#new-po-line').click(function() {
+    launchModalForm("{% url 'po-line-item-create' %}",
+        {
+            reload: true,
+            data: {
+                order: {{ order.id }},
+            },
+        }
+    );
+});
+
+$("#po-lines-table").bootstrapTable({
+});
 
 {% endblock %}
\ No newline at end of file
diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html
index f85de6c962..4e48effa59 100644
--- a/InvenTree/order/templates/order/purchase_orders.html
+++ b/InvenTree/order/templates/order/purchase_orders.html
@@ -2,6 +2,10 @@
 
 {% load static %}
 
+{% block page_title %}
+InvenTree | Purchase Orders
+{% endblock %}
+
 {% block content %}
 
 <h4>Purchase Orders</h4>
diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py
index 961cbb6857..9aa350fc54 100644
--- a/InvenTree/order/urls.py
+++ b/InvenTree/order/urls.py
@@ -14,11 +14,18 @@ purchase_order_detail_urls = [
     url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'),
 ]
 
+po_line_urls = [
+
+    url(r'^new/', views.POLineItemCreate.as_view(), name='po-line-item-create'),
+]
+
 purchase_order_urls = [
 
     # Display detail view for a single purchase order
     url(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)),
 
+    url(r'^line/', include(po_line_urls)),
+
     # Display complete list of purchase orders
     url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='purchase-order-index'),
 ]
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index add27967d7..ac84ee1f4f 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -7,7 +7,10 @@ from __future__ import unicode_literals
 
 from django.views.generic import DetailView, ListView
 
-from .models import PurchaseOrder
+from .models import PurchaseOrder, PurchaseOrderLineItem
+from .forms import EditPurchaseOrderLineItemForm
+
+from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
 
 from InvenTree.status_codes import OrderStatus
 
@@ -19,8 +22,8 @@ class PurchaseOrderIndex(ListView):
     template_name = 'order/purchase_orders.html'
     context_object_name = 'orders'
 
-    def get_context_data(self):
-        ctx = super().get_context_data()
+    def get_context_data(self, **kwargs):
+        ctx = super().get_context_data(**kwargs)
 
         ctx['OrderStatus'] = OrderStatus
 
@@ -33,3 +36,76 @@ class PurchaseOrderDetail(DetailView):
     context_object_name = 'order'
     queryset = PurchaseOrder.objects.all().prefetch_related('lines')
     template_name = 'order/purchase_order_detail.html'
+
+    def get_context_data(self, **kwargs):
+        ctx = super().get_context_data(**kwargs)
+
+        ctx['OrderStatus'] = OrderStatus
+
+        return ctx
+
+
+class POLineItemCreate(AjaxCreateView):
+    """ AJAX view for creating a new PurchaseOrderLineItem object
+    """
+
+    model = PurchaseOrderLineItem
+    context_object_name = 'line'
+    form_class = EditPurchaseOrderLineItemForm
+    ajax_template_name = 'modal_form.html'
+    ajax_form_action = 'Add Line Item'
+
+    def get_form(self):
+        """ Limit choice options based on the selected order, etc
+        """
+
+        form = super().get_form()
+
+        order_id = form['order'].value()
+
+        try:
+            order = PurchaseOrder.objects.get(id=order_id)
+
+            query = form.fields['part'].queryset
+
+            # Only allow parts from the selected supplier
+            query = query.filter(supplier=order.supplier.id)
+
+            print('limiting queryset')
+
+            form.fields['part'].queryset = query
+        except PurchaseOrder.DoesNotExist:
+            print('error')
+            pass
+
+        return form
+
+
+    def get_initial(self):
+        """ Extract initial data for the line item.
+
+        - The 'order' will be passed as a query parameter
+        - Use this to set the 'order' field and limit the options for 'part'
+        """
+
+        initials = super().get_initial().copy()
+
+        order_id = self.request.GET.get('order', None)
+
+        if order_id:
+            try:
+                order = PurchaseOrder.objects.get(id=order_id)
+                initials['order'] = order
+
+            except PurchaseOrder.DoesNotExist:
+                pass
+
+        return initials
+
+
+class POLineItemEdit(AjaxUpdateView):
+
+    model = PurchaseOrderLineItem
+    form_class = EditPurchaseOrderLineItemForm
+    ajax_template_name = 'modal_form.html'
+    ajax_form_action = 'Edit Line Item'
diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css
index da7d38d250..794676ca68 100644
--- a/InvenTree/static/css/inventree.css
+++ b/InvenTree/static/css/inventree.css
@@ -297,6 +297,10 @@
     margin-bottom: 5px;
 }
 
+.panel-body {
+    padding: 10px;
+}
+
 .panel-group .panel {
     border-radius: 2px;
 }