mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
1dea5f1624
@ -4,7 +4,7 @@ Provides information on the current InvenTree version
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.0.1"
|
INVENTREE_SW_VERSION = "0.0.2"
|
||||||
|
|
||||||
|
|
||||||
def inventreeVersion():
|
def inventreeVersion():
|
||||||
@ -14,6 +14,8 @@ def inventreeVersion():
|
|||||||
|
|
||||||
def inventreeCommitHash():
|
def inventreeCommitHash():
|
||||||
""" Returns the git commit hash for the running codebase """
|
""" Returns the git commit hash for the running codebase """
|
||||||
|
|
||||||
|
# TODO - This doesn't seem to work when running under gunicorn. Why is this?!
|
||||||
commit = str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
commit = str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
||||||
|
|
||||||
return commit
|
return commit
|
||||||
|
@ -170,7 +170,7 @@ class AjaxView(AjaxMixin, View):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return JsonResponse('', safe=False)
|
return self.renderJsonResponse(request)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
@ -62,12 +62,23 @@ InvenTree | Allocate Parts
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
$("#build-list").bootstrapTable({});
|
$("#build-list").bootstrapTable({
|
||||||
|
search: true,
|
||||||
|
sortable: true,
|
||||||
|
});
|
||||||
|
|
||||||
$("#btn-allocate").click(function() {
|
$("#btn-allocate").click(function() {
|
||||||
location.href = "{% url 'build-allocate' build.id %}?edit=1";
|
location.href = "{% url 'build-allocate' build.id %}?edit=1";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btn-order-parts").click(function() {
|
||||||
|
launchModalForm("/order/purchase-order/order-parts/", {
|
||||||
|
data: {
|
||||||
|
build: {{ build.id }},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,17 +3,19 @@
|
|||||||
<div id='#build-item-toolbar'>
|
<div id='#build-item-toolbar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>Allocate</button>
|
<button class='btn btn-primary' type='button' id='btn-allocate' title='Allocate Stock'>Allocate</button>
|
||||||
|
<button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>Order Parts</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='build-list' data-sorting='true' data-toolbar='#build-item-toolbar'>
|
<table class='table table-striped table-condensed' id='build-list' data-sorting='true' data-toolbar='#build-item-toolbar'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Part</th>
|
<th data-sortable='true'>Part</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Available</th>
|
<th data-sortable='true'>Available</th>
|
||||||
<th>Required</th>
|
<th data-sortable='true'>Required</th>
|
||||||
<th>Allocated</th>
|
<th data-sortable='true'>Allocated</th>
|
||||||
|
<th data-sortable='true'>On Order</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -27,6 +29,7 @@
|
|||||||
<td>{{ item.part.total_stock }}</td>
|
<td>{{ item.part.total_stock }}</td>
|
||||||
<td>{{ item.quantity }}</td>
|
<td>{{ item.quantity }}</td>
|
||||||
<td>{{ item.allocated }}</td>
|
<td>{{ item.allocated }}</td>
|
||||||
|
<td>{{ item.part.on_order }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -275,6 +275,7 @@ class BuildDetail(DetailView):
|
|||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
|
||||||
ctx['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
ctx['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
||||||
|
ctx['BuildStatus'] = BuildStatus
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@ -296,6 +297,7 @@ class BuildAllocate(DetailView):
|
|||||||
|
|
||||||
context['part'] = part
|
context['part'] = part
|
||||||
context['bom_items'] = bom_items
|
context['bom_items'] = bom_items
|
||||||
|
context['BuildStatus'] = BuildStatus
|
||||||
|
|
||||||
context['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
context['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
||||||
<span class="caret"></span></button>
|
<span class="caret"></span></button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href='#' id='multi-part-order' title='Order parts'>Order Parts</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,4 +102,20 @@
|
|||||||
url: "{% url 'api-part-supplier-list' %}"
|
url: "{% url 'api-part-supplier-list' %}"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#multi-part-order").click(function() {
|
||||||
|
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
var parts = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
parts.push(item.part);
|
||||||
|
});
|
||||||
|
|
||||||
|
launchModalForm("/order/purchase-order/order-parts/", {
|
||||||
|
data: {
|
||||||
|
parts: parts,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -47,5 +47,5 @@ class EditPurchaseOrderLineItemForm(HelperForm):
|
|||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
'received'
|
'notes',
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.2 on 2019-06-13 11:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0009_auto_20190606_2133'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='notes',
|
||||||
|
field=models.CharField(blank=True, help_text='Line item notes', max_length=500),
|
||||||
|
),
|
||||||
|
]
|
@ -6,10 +6,12 @@ Order model definitions
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
import tablib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
@ -98,9 +100,101 @@ class PurchaseOrder(Order):
|
|||||||
help_text=_('Company')
|
help_text=_('Company')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def export_to_file(self, **kwargs):
|
||||||
|
""" Export order information to external file """
|
||||||
|
|
||||||
|
file_format = kwargs.get('format', 'csv').lower()
|
||||||
|
|
||||||
|
data = tablib.Dataset(headers=[
|
||||||
|
'Line',
|
||||||
|
'Part',
|
||||||
|
'Description',
|
||||||
|
'Manufacturer',
|
||||||
|
'MPN',
|
||||||
|
'Order Code',
|
||||||
|
'Quantity',
|
||||||
|
'Received',
|
||||||
|
'Reference',
|
||||||
|
'Notes',
|
||||||
|
])
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
for item in self.lines.all():
|
||||||
|
|
||||||
|
line = []
|
||||||
|
|
||||||
|
line.append(idx)
|
||||||
|
|
||||||
|
if item.part:
|
||||||
|
line.append(item.part.part.name)
|
||||||
|
line.append(item.part.part.description)
|
||||||
|
|
||||||
|
line.append(item.part.manufacturer)
|
||||||
|
line.append(item.part.MPN)
|
||||||
|
line.append(item.part.SKU)
|
||||||
|
|
||||||
|
else:
|
||||||
|
line += [[] * 5]
|
||||||
|
|
||||||
|
line.append(item.quantity)
|
||||||
|
line.append(item.received)
|
||||||
|
line.append(item.reference)
|
||||||
|
line.append(item.notes)
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
data.append(line)
|
||||||
|
|
||||||
|
return data.export(file_format)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('purchase-order-detail', kwargs={'pk': self.id})
|
return reverse('purchase-order-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
def add_line_item(self, supplier_part, quantity, group=True, reference=''):
|
||||||
|
""" Add a new line item to this purchase order.
|
||||||
|
This function will check that:
|
||||||
|
|
||||||
|
* The supplier part matches the supplier specified for this purchase order
|
||||||
|
* The quantity is greater than zero
|
||||||
|
|
||||||
|
Args:
|
||||||
|
supplier_part - The supplier_part to add
|
||||||
|
quantity - The number of items to add
|
||||||
|
group - If True, this new quantity will be added to an existing line item for the same supplier_part (if it exists)
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
quantity = int(quantity)
|
||||||
|
if quantity <= 0:
|
||||||
|
raise ValidationError({
|
||||||
|
'quantity': _("Quantity must be greater than zero")})
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError({'quantity': _("Invalid quantity provided")})
|
||||||
|
|
||||||
|
if not supplier_part.supplier == self.supplier:
|
||||||
|
raise ValidationError({'supplier': _("Part supplier must match PO supplier")})
|
||||||
|
|
||||||
|
if group:
|
||||||
|
# Check if there is already a matching line item
|
||||||
|
matches = PurchaseOrderLineItem.objects.filter(part=supplier_part)
|
||||||
|
|
||||||
|
if matches.count() > 0:
|
||||||
|
line = matches.first()
|
||||||
|
|
||||||
|
line.quantity += quantity
|
||||||
|
line.save()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
line = PurchaseOrderLineItem(
|
||||||
|
order=self,
|
||||||
|
part=supplier_part,
|
||||||
|
quantity=quantity,
|
||||||
|
reference=reference)
|
||||||
|
|
||||||
|
line.save()
|
||||||
|
|
||||||
|
|
||||||
class OrderLineItem(models.Model):
|
class OrderLineItem(models.Model):
|
||||||
""" Abstract model for an order line item
|
""" Abstract model for an order line item
|
||||||
@ -118,6 +212,8 @@ class OrderLineItem(models.Model):
|
|||||||
|
|
||||||
reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference'))
|
reference = models.CharField(max_length=100, blank=True, help_text=_('Line item reference'))
|
||||||
|
|
||||||
|
notes = models.CharField(max_length=500, blank=True, help_text=_('Line item notes'))
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItem(OrderLineItem):
|
class PurchaseOrderLineItem(OrderLineItem):
|
||||||
""" Model for a purchase order line item.
|
""" Model for a purchase order line item.
|
||||||
@ -132,6 +228,13 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
('order', 'part')
|
('order', 'part')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{n} x {part} from {supplier} (for {po})".format(
|
||||||
|
n=self.quantity,
|
||||||
|
part=self.part.SKU if self.part else 'unknown part',
|
||||||
|
supplier=self.order.supplier.name,
|
||||||
|
po=self.order)
|
||||||
|
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
PurchaseOrder, on_delete=models.CASCADE,
|
PurchaseOrder, on_delete=models.CASCADE,
|
||||||
related_name='lines',
|
related_name='lines',
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
|
||||||
|
<h4>
|
||||||
|
Step 1 of 2 - Select Parts
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{% if parts|length > 0 %}
|
||||||
|
<div class='alert alert-info alert-block' role='alert'>
|
||||||
|
Select suppliers for {{ parts|length }} parts.
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class='alert alert-warning alert-block' role='alert'>
|
||||||
|
No purchaseable parts selected.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
<input type='hidden' name='form_step' value='select_parts'/>
|
||||||
|
|
||||||
|
<table class='table table-condensed table-striped' id='order-wizard-part-table'>
|
||||||
|
<tr>
|
||||||
|
<th>Part</th>
|
||||||
|
<th colspan='2'>Select Supplier</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
{% for part in parts %}
|
||||||
|
<tr id='part_row_{{ part.id }}'>
|
||||||
|
<td>
|
||||||
|
{% include "hover_image.html" with image=part.image hover=False %}
|
||||||
|
{{ part.full_name }} <small><i>{{ part.description }}</i></small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class='btn btn-default btn-create' id='new_supplier_part_{{ part.id }}' title='Create new supplier part for {{ part }}' type='button'>
|
||||||
|
<span part-id='{{ part.id }}' onClick='newSupplierPartFromOrderWizard()' class='glyphicon glyphicon-small glyphicon-plus'></span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class='control-group'>
|
||||||
|
<div class='controls'>
|
||||||
|
<select class='select' id='id_supplier_part_{{ part.id }}' name="part-supplier-{{ part.id }}">
|
||||||
|
<option value=''>---------</option>
|
||||||
|
{% for supplier in part.supplier_parts.all %}
|
||||||
|
<option value="{{ supplier.id }}"{% if part.order_supplier == supplier.id %} selected="selected"{% endif %}>{{ supplier }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% if not part.order_supplier %}
|
||||||
|
<span class='help-inline'>Select a supplier for <i>{{ part.name }}</i></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class='control-group'>
|
||||||
|
<div class='controls'>
|
||||||
|
<input class='numberinput' type='number' min='0' value='{{ part.order_quantity }}' name='part-quantity-{{ part.id }}'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class='btn btn-default btn-remove' id='del_item_{{ part.id }}' title='Remove part' type='button'>
|
||||||
|
<span row='part_row_{{ part.id }}' onclick='removeOrderRowFromOrderWizard()' class='glyphicon glyphicon-small glyphicon-remove'></span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
73
InvenTree/order/templates/order/order_wizard/select_pos.html
Normal file
73
InvenTree/order/templates/order/order_wizard/select_pos.html
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
|
||||||
|
<h4>
|
||||||
|
Step 2 of 2 - Select Purchase Orders
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class='alert alert-info alert-block' role='alert'>
|
||||||
|
Select existing purchase orders, or create new orders.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
<input type='hidden' name='form_step' value='select_purchase_orders'/>
|
||||||
|
|
||||||
|
{% for supplier in suppliers %}
|
||||||
|
{% for item in supplier.order_items %}
|
||||||
|
<input type='hidden' name='part-supplier-{{ item.id }}' value='{{ item.order_supplier }}'/>
|
||||||
|
<input type='hidden' name='part-quantity-{{ item.id }}' value='{{ item.order_quantity }}'/>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<table class='table table-condensed table-striped' id='order-wizard-po-table'>
|
||||||
|
<tr>
|
||||||
|
<th>Supplier</th>
|
||||||
|
<th>Items</th>
|
||||||
|
<th colspan='2'>Select Purchase Order</th>
|
||||||
|
</tr>
|
||||||
|
{% for supplier in suppliers %}
|
||||||
|
<tr id='suppier_row_{{ supplier.id }}'>
|
||||||
|
<td>
|
||||||
|
{% include 'hover_image.html' with image=supplier.image hover=False %}
|
||||||
|
{{ supplier.name }}
|
||||||
|
</td>
|
||||||
|
<td>{{ supplier.order_items|length }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class='btn btn-default btn-create'
|
||||||
|
id='new_po_{{ supplier.id }}'
|
||||||
|
title='Create new purchase order for {{ supplier.name }}'
|
||||||
|
type='button'>
|
||||||
|
<span supplier-id='{{ supplier.id }}' onclick='newPurchaseOrderFromOrderWizard()' class='glyphicon glyphicon-small glyphicon-plus'></span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class='control-group'>
|
||||||
|
<div class='controls'>
|
||||||
|
<select
|
||||||
|
class='select'
|
||||||
|
id='id-purchase-order-{{ supplier.id }}'
|
||||||
|
name='purchase-order-{{ supplier.id }}'>
|
||||||
|
<option value=''>---------</option>
|
||||||
|
{% for order in supplier.outstanding_purchase_orders %}
|
||||||
|
<option value='{{ order.id }}'{% if supplier.selected_purchase_order == order.id %} selected='selected'{% endif %}>
|
||||||
|
{{ order }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% if not supplier.selected_purchase_order %}
|
||||||
|
<span class='help-inline'>Select a purchase order for {{ supplier.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
5
InvenTree/order/templates/order/po_lineitem_delete.html
Normal file
5
InvenTree/order/templates/order/po_lineitem_delete.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "modal_delete_form.html" %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
Are you sure you wish to delete this line item?
|
||||||
|
{% endblock %}
|
@ -4,6 +4,7 @@
|
|||||||
<th data-field='reference'>Order Reference</th>
|
<th data-field='reference'>Order Reference</th>
|
||||||
<th data-field='description'>Description</th>
|
<th data-field='description'>Description</th>
|
||||||
<th data-field='status'>Status</th>
|
<th data-field='status'>Status</th>
|
||||||
|
<th data-field='items'>Items</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for order in orders %}
|
{% for order in orders %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -11,6 +12,7 @@
|
|||||||
<td><a href="{% url 'purchase-order-detail' order.id %}">{{ order }}</a></td>
|
<td><a href="{% url 'purchase-order-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>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
@ -64,6 +64,7 @@ InvenTree | {{ order }}
|
|||||||
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
|
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
|
||||||
<button type='button' class='btn btn-primary' id='place-order'>Place Order</button>
|
<button type='button' class='btn btn-primary' id='place-order'>Place Order</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<button type='button' class='btn btn-primary' id='export-order' title='Export order to file'>Export</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>Order Items</h4>
|
<h4>Order Items</h4>
|
||||||
@ -79,20 +80,48 @@ InvenTree | {{ order }}
|
|||||||
<th data-field='sku'>Order Code</th>
|
<th data-field='sku'>Order Code</th>
|
||||||
<th data-field='reference'>Reference</th>
|
<th data-field='reference'>Reference</th>
|
||||||
<th data-field='quantity'>Quantity</th>
|
<th data-field='quantity'>Quantity</th>
|
||||||
|
{% if not order.status == OrderStatus.PENDING %}
|
||||||
<th data-field='received'>Received</th>
|
<th data-field='received'>Received</th>
|
||||||
|
{% endif %}
|
||||||
|
<th data-field='notes'>Note</th>
|
||||||
|
{% if order.status == OrderStatus.PENDING %}
|
||||||
|
<th data-field='buttons'></th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% for line in order.lines.all %}
|
{% for line in order.lines.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ forloop.counter }}</td>
|
<td>
|
||||||
|
{{ forloop.counter }}
|
||||||
|
</td>
|
||||||
{% if line.part %}
|
{% if line.part %}
|
||||||
<td><a href="{% url 'part-detail' line.part.part.id %}">{{ line.part.part.full_name }}</a></td>
|
<td>
|
||||||
|
{% include "hover_image.html" with image=line.part.part.image hover=True %}
|
||||||
|
<a href="{% url 'part-detail' line.part.part.id %}">{{ line.part.part.full_name }}</a>
|
||||||
|
</td>
|
||||||
<td><a href="{% url 'supplier-part-detail' line.part.id %}">{{ line.part.SKU }}</a></td>
|
<td><a href="{% url 'supplier-part-detail' line.part.id %}">{{ line.part.SKU }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan='2'><strong>Warning: Part has been deleted.</strong></td>
|
<td colspan='2'><strong>Warning: Part has been deleted.</strong></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ line.reference }}</td>
|
<td>{{ line.reference }}</td>
|
||||||
<td>{{ line.quantity }}</td>
|
<td>{{ line.quantity }}</td>
|
||||||
|
{% if not order.status == OrderStatus.PENDING %}
|
||||||
<td>{{ line.received }}</td>
|
<td>{{ line.received }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
{{ line.notes }}
|
||||||
|
</td>
|
||||||
|
{% if order.status == OrderStatus.PENDING %}
|
||||||
|
<td>
|
||||||
|
<div class='btn-group'>
|
||||||
|
<button class='btn btn-default btn-edit' id='edit-line-item-{{ line.id }} title='Edit line item' onclick='editPurchaseOrderLineItem()'>
|
||||||
|
<span url="{% url 'po-line-item-edit' line.id %}" line='{{ line.id }}' class='glyphicon glyphicon-small glyphicon-edit'></span>
|
||||||
|
</button>
|
||||||
|
<button class='btn btn-default btn-remove' id='remove-line-item-{{ line.id }' title='Remove line item' type='button' onclick='removePurchaseOrderLineItem()'>
|
||||||
|
<span url="{% url 'po-line-item-delete' line.id %}" line='{{ line.id }}' class='glyphicon glyphicon-small glyphicon-remove'></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
@ -124,6 +153,10 @@ $("#edit-order").click(function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#export-order").click(function() {
|
||||||
|
location.href = "{% url 'purchase-order-export' order.id %}";
|
||||||
|
});
|
||||||
|
|
||||||
{% if order.status == OrderStatus.PENDING %}
|
{% if order.status == OrderStatus.PENDING %}
|
||||||
$('#new-po-line').click(function() {
|
$('#new-po-line').click(function() {
|
||||||
launchModalForm("{% url 'po-line-item-create' %}",
|
launchModalForm("{% url 'po-line-item-create' %}",
|
||||||
@ -151,4 +184,5 @@ $('#new-po-line').click(function() {
|
|||||||
$("#po-lines-table").bootstrapTable({
|
$("#po-lines-table").bootstrapTable({
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -14,18 +14,30 @@ purchase_order_detail_urls = [
|
|||||||
url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='purchase-order-edit'),
|
url(r'^edit/?', views.PurchaseOrderEdit.as_view(), name='purchase-order-edit'),
|
||||||
url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='purchase-order-issue'),
|
url(r'^issue/?', views.PurchaseOrderIssue.as_view(), name='purchase-order-issue'),
|
||||||
|
|
||||||
|
url(r'^export/?', views.PurchaseOrderExport.as_view(), name='purchase-order-export'),
|
||||||
|
|
||||||
url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'),
|
url(r'^.*$', views.PurchaseOrderDetail.as_view(), name='purchase-order-detail'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
po_line_item_detail_urls = [
|
||||||
|
|
||||||
|
url(r'^edit/', views.POLineItemEdit.as_view(), name='po-line-item-edit'),
|
||||||
|
url(r'^delete/', views.POLineItemDelete.as_view(), name='po-line-item-delete'),
|
||||||
|
]
|
||||||
|
|
||||||
po_line_urls = [
|
po_line_urls = [
|
||||||
|
|
||||||
url(r'^new/', views.POLineItemCreate.as_view(), name='po-line-item-create'),
|
url(r'^new/', views.POLineItemCreate.as_view(), name='po-line-item-create'),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/', include(po_line_item_detail_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='purchase-order-create'),
|
||||||
|
|
||||||
|
url(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'),
|
||||||
|
|
||||||
# Display detail view for a single purchase order
|
# Display detail view for a single purchase order
|
||||||
url(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)),
|
url(r'^(?P<pk>\d+)/', include(purchase_order_detail_urls)),
|
||||||
|
|
||||||
|
@ -5,20 +5,28 @@ Django views for interacting with Order app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||||
|
from build.models import Build
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
|
from stock.models import StockItem
|
||||||
|
from part.models import Part
|
||||||
|
|
||||||
from . import forms as order_forms
|
from . import forms as order_forms
|
||||||
|
|
||||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView
|
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
|
|
||||||
from InvenTree.status_codes import OrderStatus
|
from InvenTree.status_codes import OrderStatus
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderIndex(ListView):
|
class PurchaseOrderIndex(ListView):
|
||||||
""" List view for all purchase orders """
|
""" List view for all purchase orders """
|
||||||
@ -135,6 +143,331 @@ class PurchaseOrderIssue(AjaxUpdateView):
|
|||||||
return self.renderJsonResponse(request, form, data)
|
return self.renderJsonResponse(request, form, data)
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderExport(AjaxView):
|
||||||
|
""" File download for a purchase order
|
||||||
|
|
||||||
|
- File format can be optionally passed as a query param e.g. ?format=CSV
|
||||||
|
- Default file format is CSV
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = PurchaseOrder
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
export_format = request.GET.get('format', 'csv')
|
||||||
|
|
||||||
|
filename = str(order) + '.' + export_format
|
||||||
|
|
||||||
|
filedata = order.export_to_file(format=export_format)
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderParts(AjaxView):
|
||||||
|
""" View for adding various SupplierPart items to a Purchase Order.
|
||||||
|
|
||||||
|
SupplierParts can be selected from a variety of 'sources':
|
||||||
|
|
||||||
|
- ?supplier_parts[]= -> Direct list of SupplierPart objects
|
||||||
|
- ?parts[]= -> List of base Part objects (user must then select supplier parts)
|
||||||
|
- ?stock[]= -> List of StockItem objects (user must select supplier parts)
|
||||||
|
- ?build= -> A Build object (user must select parts, then supplier parts)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ajax_form_title = "Order Parts"
|
||||||
|
ajax_template_name = 'order/order_wizard/select_parts.html'
|
||||||
|
|
||||||
|
# List of Parts we wish to order
|
||||||
|
parts = []
|
||||||
|
suppliers = []
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
|
||||||
|
ctx = {}
|
||||||
|
|
||||||
|
ctx['parts'] = sorted(self.parts, key=lambda part: int(part.order_quantity), reverse=True)
|
||||||
|
ctx['suppliers'] = self.suppliers
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_suppliers(self):
|
||||||
|
""" Calculates a list of suppliers which the user will need to create POs for.
|
||||||
|
This is calculated AFTER the user finishes selecting the parts to order.
|
||||||
|
Crucially, get_parts() must be called before get_suppliers()
|
||||||
|
"""
|
||||||
|
|
||||||
|
suppliers = {}
|
||||||
|
|
||||||
|
for supplier in self.suppliers:
|
||||||
|
supplier.order_items = []
|
||||||
|
suppliers[supplier.name] = supplier
|
||||||
|
|
||||||
|
for part in self.parts:
|
||||||
|
supplier_part_id = part.order_supplier
|
||||||
|
|
||||||
|
try:
|
||||||
|
supplier = SupplierPart.objects.get(pk=supplier_part_id).supplier
|
||||||
|
except SupplierPart.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if supplier.name not in suppliers:
|
||||||
|
supplier.order_items = []
|
||||||
|
supplier.selected_purchase_order = None
|
||||||
|
suppliers[supplier.name] = supplier
|
||||||
|
|
||||||
|
suppliers[supplier.name].order_items.append(part)
|
||||||
|
|
||||||
|
self.suppliers = [suppliers[key] for key in suppliers.keys()]
|
||||||
|
|
||||||
|
def get_parts(self):
|
||||||
|
""" Determine which parts the user wishes to order.
|
||||||
|
This is performed on the initial GET request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.parts = []
|
||||||
|
|
||||||
|
part_ids = set()
|
||||||
|
|
||||||
|
# User has passed a list of stock items
|
||||||
|
if 'stock[]' in self.request.GET:
|
||||||
|
|
||||||
|
stock_id_list = self.request.GET.getlist('stock[]')
|
||||||
|
|
||||||
|
""" Get a list of all the parts associated with the stock items.
|
||||||
|
- Base part must be purchaseable.
|
||||||
|
- Return a set of corresponding Part IDs
|
||||||
|
"""
|
||||||
|
stock_items = StockItem.objects.filter(
|
||||||
|
part__purchaseable=True,
|
||||||
|
id__in=stock_id_list)
|
||||||
|
|
||||||
|
for item in stock_items:
|
||||||
|
part_ids.add(item.part.id)
|
||||||
|
|
||||||
|
# User has passed a single Part ID
|
||||||
|
elif 'part' in self.request.GET:
|
||||||
|
try:
|
||||||
|
part_id = self.request.GET.get('part')
|
||||||
|
part = Part.objects.get(id=part_id)
|
||||||
|
|
||||||
|
part_ids.add(part.id)
|
||||||
|
|
||||||
|
except Part.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# User has passed a list of part ID values
|
||||||
|
elif 'parts[]' in self.request.GET:
|
||||||
|
part_id_list = self.request.GET.getlist('parts[]')
|
||||||
|
|
||||||
|
parts = Part.objects.filter(
|
||||||
|
purchaseable=True,
|
||||||
|
id__in=part_id_list)
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
part_ids.add(part.id)
|
||||||
|
|
||||||
|
# User has provided a Build ID
|
||||||
|
elif 'build' in self.request.GET:
|
||||||
|
build_id = self.request.GET.get('build')
|
||||||
|
try:
|
||||||
|
build = Build.objects.get(id=build_id)
|
||||||
|
|
||||||
|
parts = build.part.required_parts()
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
# If ordering from a Build page, ignore parts that we have enough of
|
||||||
|
if part.quantity_to_order <= 0:
|
||||||
|
continue
|
||||||
|
part_ids.add(part.id)
|
||||||
|
except Build.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create the list of parts
|
||||||
|
for id in part_ids:
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(id=id)
|
||||||
|
# Pre-fill the 'order quantity' value
|
||||||
|
part.order_quantity = part.quantity_to_order
|
||||||
|
|
||||||
|
default_supplier = part.get_default_supplier()
|
||||||
|
|
||||||
|
if default_supplier:
|
||||||
|
part.order_supplier = default_supplier.id
|
||||||
|
else:
|
||||||
|
part.order_supplier = None
|
||||||
|
except Part.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.parts.append(part)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
self.get_parts()
|
||||||
|
|
||||||
|
return self.renderJsonResponse(request)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
""" Handle the POST action for part selection.
|
||||||
|
|
||||||
|
- Validates each part / quantity / supplier / etc
|
||||||
|
|
||||||
|
Part selection form contains the following fields for each part:
|
||||||
|
|
||||||
|
- supplier-<pk> : The ID of the selected supplier
|
||||||
|
- quantity-<pk> : The quantity to add to the order
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
self.parts = []
|
||||||
|
self.suppliers = []
|
||||||
|
|
||||||
|
# Any errors for the part selection form?
|
||||||
|
part_errors = False
|
||||||
|
supplier_errors = False
|
||||||
|
|
||||||
|
# Extract part information from the form
|
||||||
|
for item in self.request.POST:
|
||||||
|
|
||||||
|
if item.startswith('part-supplier-'):
|
||||||
|
|
||||||
|
pk = item.replace('part-supplier-', '')
|
||||||
|
|
||||||
|
# Check that the part actually exists
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(id=pk)
|
||||||
|
except (Part.DoesNotExist, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
supplier_part_id = self.request.POST[item]
|
||||||
|
|
||||||
|
quantity = self.request.POST.get('part-quantity-' + str(pk), 0)
|
||||||
|
|
||||||
|
# Ensure a valid supplier has been passed
|
||||||
|
try:
|
||||||
|
supplier_part = SupplierPart.objects.get(id=supplier_part_id)
|
||||||
|
except (SupplierPart.DoesNotExist, ValueError):
|
||||||
|
supplier_part = None
|
||||||
|
|
||||||
|
# Ensure a valid quantity is passed
|
||||||
|
try:
|
||||||
|
quantity = int(quantity)
|
||||||
|
|
||||||
|
# Eliminate lines where the quantity is zero
|
||||||
|
if quantity == 0:
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
quantity = part.quantity_to_order
|
||||||
|
|
||||||
|
part.order_supplier = supplier_part.id if supplier_part else None
|
||||||
|
part.order_quantity = quantity
|
||||||
|
|
||||||
|
self.parts.append(part)
|
||||||
|
|
||||||
|
if supplier_part is None:
|
||||||
|
part_errors = True
|
||||||
|
|
||||||
|
elif quantity < 0:
|
||||||
|
part_errors = True
|
||||||
|
|
||||||
|
elif item.startswith('purchase-order-'):
|
||||||
|
# Which purchase order is selected for a given supplier?
|
||||||
|
pk = item.replace('purchase-order-', '')
|
||||||
|
|
||||||
|
# Check that the Supplier actually exists
|
||||||
|
try:
|
||||||
|
supplier = Company.objects.get(id=pk)
|
||||||
|
except Company.DoesNotExist:
|
||||||
|
# Skip this item
|
||||||
|
continue
|
||||||
|
|
||||||
|
purchase_order_id = self.request.POST[item]
|
||||||
|
|
||||||
|
# Ensure that a valid purchase order has been passed
|
||||||
|
try:
|
||||||
|
purchase_order = PurchaseOrder.objects.get(pk=purchase_order_id)
|
||||||
|
except (PurchaseOrder.DoesNotExist, ValueError):
|
||||||
|
purchase_order = None
|
||||||
|
|
||||||
|
supplier.selected_purchase_order = purchase_order.id if purchase_order else None
|
||||||
|
|
||||||
|
self.suppliers.append(supplier)
|
||||||
|
|
||||||
|
if supplier.selected_purchase_order is None:
|
||||||
|
supplier_errors = True
|
||||||
|
|
||||||
|
form_step = request.POST.get('form_step')
|
||||||
|
|
||||||
|
# Map parts to suppliers
|
||||||
|
self.get_suppliers()
|
||||||
|
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
if form_step == 'select_parts':
|
||||||
|
# No errors? Proceed to PO selection form
|
||||||
|
if part_errors is False:
|
||||||
|
self.ajax_template_name = 'order/order_wizard/select_pos.html'
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.ajax_template_name = 'order/order_wizard/select_parts.html'
|
||||||
|
|
||||||
|
elif form_step == 'select_purchase_orders':
|
||||||
|
|
||||||
|
self.ajax_template_name = 'order/order_wizard/select_pos.html'
|
||||||
|
|
||||||
|
valid = part_errors is False and supplier_errors is False
|
||||||
|
|
||||||
|
# Form wizard is complete! Add items to purchase orders
|
||||||
|
if valid:
|
||||||
|
self.order_items()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'form_valid': valid,
|
||||||
|
'success': 'Ordered {n} parts'.format(n=len(self.parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.renderJsonResponse(self.request, data=data)
|
||||||
|
|
||||||
|
def order_items(self):
|
||||||
|
""" Add the selected items to the purchase orders. """
|
||||||
|
|
||||||
|
for supplier in self.suppliers:
|
||||||
|
|
||||||
|
# Check that the purchase order does actually exist
|
||||||
|
try:
|
||||||
|
order = PurchaseOrder.objects.get(pk=supplier.selected_purchase_order)
|
||||||
|
except PurchaseOrder.DoesNotExist:
|
||||||
|
logger.critical('Could not add items to purchase order {po} - Order does not exist'.format(po=supplier.selected_purchase_order))
|
||||||
|
continue
|
||||||
|
|
||||||
|
for item in supplier.order_items:
|
||||||
|
|
||||||
|
# Ensure that the quantity is valid
|
||||||
|
try:
|
||||||
|
quantity = int(item.order_quantity)
|
||||||
|
if quantity <= 0:
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
logger.warning("Did not add part to purchase order - incorrect quantity")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check that the supplier part does actually exist
|
||||||
|
try:
|
||||||
|
supplier_part = SupplierPart.objects.get(pk=item.order_supplier)
|
||||||
|
except SupplierPart.DoesNotExist:
|
||||||
|
logger.critical("Could not add part '{part}' to purchase order - selected supplier part '{sp}' does not exist.".format(
|
||||||
|
part=item,
|
||||||
|
sp=item.order_supplier))
|
||||||
|
continue
|
||||||
|
|
||||||
|
order.add_line_item(supplier_part, quantity)
|
||||||
|
|
||||||
|
|
||||||
class POLineItemCreate(AjaxCreateView):
|
class POLineItemCreate(AjaxCreateView):
|
||||||
""" AJAX view for creating a new PurchaseOrderLineItem object
|
""" AJAX view for creating a new PurchaseOrderLineItem object
|
||||||
"""
|
"""
|
||||||
@ -229,8 +562,32 @@ class POLineItemCreate(AjaxCreateView):
|
|||||||
|
|
||||||
|
|
||||||
class POLineItemEdit(AjaxUpdateView):
|
class POLineItemEdit(AjaxUpdateView):
|
||||||
|
""" View for editing a PurchaseOrderLineItem object in a modal form.
|
||||||
|
"""
|
||||||
|
|
||||||
model = PurchaseOrderLineItem
|
model = PurchaseOrderLineItem
|
||||||
form_class = order_forms.EditPurchaseOrderLineItemForm
|
form_class = order_forms.EditPurchaseOrderLineItemForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_action = 'Edit Line Item'
|
ajax_form_title = 'Edit Line Item'
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
form = super().get_form()
|
||||||
|
|
||||||
|
# Prevent user from editing order once line item is assigned
|
||||||
|
form.fields.pop('order')
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
class POLineItemDelete(AjaxDeleteView):
|
||||||
|
""" View for deleting a PurchaseOrderLineItem object in a modal form
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = PurchaseOrderLineItem
|
||||||
|
ajax_form_title = 'Delete Line Item'
|
||||||
|
ajax_template_name = 'order/po_lineitem_delete.html'
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return {
|
||||||
|
'danger': 'Deleted line item',
|
||||||
|
}
|
||||||
|
@ -339,7 +339,7 @@ class Part(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self.default_supplier:
|
if self.default_supplier:
|
||||||
return self.default_suppliers
|
return self.default_supplier
|
||||||
|
|
||||||
if self.supplier_count == 1:
|
if self.supplier_count == 1:
|
||||||
return self.supplier_parts.first()
|
return self.supplier_parts.first()
|
||||||
@ -410,6 +410,26 @@ class Part(models.Model):
|
|||||||
|
|
||||||
return max(total, 0)
|
return max(total, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quantity_to_order(self):
|
||||||
|
""" Return the quantity needing to be ordered for this part. """
|
||||||
|
|
||||||
|
required = -1 * self.net_stock
|
||||||
|
return max(required, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def net_stock(self):
|
||||||
|
""" Return the 'net' stock. It takes into account:
|
||||||
|
|
||||||
|
- Stock on hand (total_stock)
|
||||||
|
- Stock on order (on_order)
|
||||||
|
- Stock allocated (allocation_count)
|
||||||
|
|
||||||
|
This number (unlike 'available_stock') can be negative.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.total_stock - self.allocation_count + self.on_order
|
||||||
|
|
||||||
def isStarredBy(self, user):
|
def isStarredBy(self, user):
|
||||||
""" Return True if this part has been starred by a particular user """
|
""" Return True if this part has been starred by a particular user """
|
||||||
|
|
||||||
|
@ -65,6 +65,8 @@ class PartSerializer(serializers.ModelSerializer):
|
|||||||
image_url = serializers.CharField(source='get_image_url', read_only=True)
|
image_url = serializers.CharField(source='get_image_url', read_only=True)
|
||||||
category_name = serializers.CharField(source='category_path', read_only=True)
|
category_name = serializers.CharField(source='category_path', read_only=True)
|
||||||
|
|
||||||
|
allocated_stock = serializers.IntegerField(source='allocation_count', read_only=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup_eager_loading(queryset):
|
def setup_eager_loading(queryset):
|
||||||
queryset = queryset.prefetch_related('category')
|
queryset = queryset.prefetch_related('category')
|
||||||
@ -91,7 +93,8 @@ class PartSerializer(serializers.ModelSerializer):
|
|||||||
'keywords',
|
'keywords',
|
||||||
'URL',
|
'URL',
|
||||||
'total_stock',
|
'total_stock',
|
||||||
# 'available_stock',
|
'allocated_stock',
|
||||||
|
'on_order',
|
||||||
'units',
|
'units',
|
||||||
'trackable',
|
'trackable',
|
||||||
'assembly',
|
'assembly',
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
<div class='dropdown' style='float: right;'>
|
<div class='dropdown' style='float: right;'>
|
||||||
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">Options<span class='caret'></span></button>
|
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">Options<span class='caret'></span></button>
|
||||||
<ul class='dropdown-menu'>
|
<ul class='dropdown-menu'>
|
||||||
|
<li><a href='#' id='multi-part-order' title='Order parts'>Order Parts</a></li>
|
||||||
<li><a href='#' id='multi-part-category' title='Set Part Category'>Set Category</a></li>
|
<li><a href='#' id='multi-part-category' title='Set Part Category'>Set Category</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if part.active %}
|
{% if part.active %}
|
||||||
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
|
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
|
||||||
|
{% if part.purchaseable %}
|
||||||
|
<li><a href='#' id='order-part' title='Order part'>Order</a></li>
|
||||||
|
{% endif %}
|
||||||
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
|
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
|
||||||
<hr>
|
<hr>
|
||||||
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
|
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
|
||||||
@ -159,6 +162,14 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#order-part").click(function() {
|
||||||
|
launchModalForm("/order/purchase-order/order-parts/", {
|
||||||
|
data: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$("#edit-part").click(function() {
|
$("#edit-part").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'part-edit' part.id %}",
|
"{% url 'part-edit' part.id %}",
|
||||||
|
@ -72,24 +72,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h4>Stock Status - {{ part.available_stock }}{% if part.units %} {{ part.units }} {% endif%} available</h4>
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<td colspan='2'>
|
||||||
|
<h4>Stock Status</h4>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>In Stock</td>
|
<td>In Stock</td>
|
||||||
<td>{{ part.total_stock }}</td>
|
<td>{{ part.total_stock }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if part.assembly %}
|
|
||||||
<tr>
|
|
||||||
<td>Can Build</td>
|
|
||||||
<td>{{ part.can_build }}</td>
|
|
||||||
</tr>
|
|
||||||
{% if part.quantity_being_built > 0 %}
|
|
||||||
<tr>
|
|
||||||
<td>Underway</td>
|
|
||||||
<td>{{ part.quantity_being_built }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if part.allocation_count > 0 %}
|
{% if part.allocation_count > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Allocated</td>
|
<td>Allocated</td>
|
||||||
@ -102,6 +94,27 @@
|
|||||||
<td>{{ part.on_order }}</td>
|
<td>{{ part.on_order }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td><b>Total Available</b></td>
|
||||||
|
<td><b>{{ part.net_stock }}</b></td>
|
||||||
|
</tr>
|
||||||
|
{% if part.assembly %}
|
||||||
|
<tr>
|
||||||
|
<td colspan='2'>
|
||||||
|
<h4>Build Status</h4>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Can Build</td>
|
||||||
|
<td>{{ part.can_build }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if part.quantity_being_built > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td>Underway</td>
|
||||||
|
<td>{{ part.quantity_being_built }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -585,10 +585,6 @@ class BomDownload(AjaxView):
|
|||||||
# TODO - This should no longer extend an AjaxView!
|
# TODO - This should no longer extend an AjaxView!
|
||||||
|
|
||||||
model = Part
|
model = Part
|
||||||
# form_class = BomExportForm
|
|
||||||
# template_name = 'part/bom_export.html'
|
|
||||||
# ajax_form_title = 'Export Bill of Materials'
|
|
||||||
# context_object_name = 'part'
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
@ -14,6 +14,10 @@
|
|||||||
color: #ffbb00;
|
color: #ffbb00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.red-cell {
|
||||||
|
background-color: #ec7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
.part-price {
|
.part-price {
|
||||||
color: rgb(13, 245, 25);
|
color: rgb(13, 245, 25);
|
||||||
}
|
}
|
||||||
@ -288,6 +292,21 @@
|
|||||||
color: #A11;
|
color: #A11;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-create {
|
||||||
|
padding: 3px;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
color: #1A1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
padding: 3px;
|
||||||
|
padding: 3px;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
color: #55E;
|
||||||
|
}
|
||||||
|
|
||||||
.button-toolbar {
|
.button-toolbar {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
101
InvenTree/static/script/inventree/order.js
Normal file
101
InvenTree/static/script/inventree/order.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
function removeOrderRowFromOrderWizard(e) {
|
||||||
|
/* Remove a part selection from an order form. */
|
||||||
|
|
||||||
|
e = e || window.event;
|
||||||
|
|
||||||
|
var src = e.target || e.srcElement;
|
||||||
|
|
||||||
|
var row = $(src).attr('row');
|
||||||
|
|
||||||
|
$('#' + row).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function newSupplierPartFromOrderWizard(e) {
|
||||||
|
/* Create a new supplier part directly from an order form.
|
||||||
|
* Launches a secondary modal and (if successful),
|
||||||
|
* back-populates the selected row.
|
||||||
|
*/
|
||||||
|
|
||||||
|
e = e || window.event;
|
||||||
|
|
||||||
|
var src = e.target || e.srcElement;
|
||||||
|
|
||||||
|
var part = $(src).attr('part-id');
|
||||||
|
|
||||||
|
launchModalForm("/supplier-part/new/", {
|
||||||
|
modal: '#modal-form-secondary',
|
||||||
|
data: {
|
||||||
|
part: part,
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
/* A new supplier part has been created! */
|
||||||
|
|
||||||
|
var dropdown = '#id_supplier_part_' + part;
|
||||||
|
|
||||||
|
var option = new Option(response.text, response.pk, true, true);
|
||||||
|
|
||||||
|
$('#modal-form').find(dropdown).append(option).trigger('change');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function newPurchaseOrderFromOrderWizard(e) {
|
||||||
|
/* Create a new purchase order directly from an order form.
|
||||||
|
* Launches a secondary modal and (if successful),
|
||||||
|
* back-fills the newly created purchase order.
|
||||||
|
*/
|
||||||
|
|
||||||
|
e = e || window.event;
|
||||||
|
|
||||||
|
var src = e.target || e.srcElement;
|
||||||
|
|
||||||
|
var supplier = $(src).attr('supplier-id');
|
||||||
|
|
||||||
|
launchModalForm("/order/purchase-order/new/", {
|
||||||
|
modal: '#modal-form-secondary',
|
||||||
|
data: {
|
||||||
|
supplier: supplier,
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
/* A new purchase order has been created! */
|
||||||
|
|
||||||
|
var dropdown = '#id-purchase-order-' + supplier;
|
||||||
|
|
||||||
|
var option = new Option(response.text, response.pk, true, true);
|
||||||
|
|
||||||
|
$('#modal-form').find(dropdown).append(option).trigger('change');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function editPurchaseOrderLineItem(e) {
|
||||||
|
|
||||||
|
/* Edit a purchase order line item in a modal form.
|
||||||
|
*/
|
||||||
|
|
||||||
|
e = e || window.event;
|
||||||
|
|
||||||
|
var src = e.target || e.srcElement;
|
||||||
|
|
||||||
|
var url = $(src).attr('url');
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
reload: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePurchaseOrderLineItem(e) {
|
||||||
|
|
||||||
|
/* Delete a purchase order line item in a modal form
|
||||||
|
*/
|
||||||
|
|
||||||
|
e = e || window.event;
|
||||||
|
|
||||||
|
var src = e.target || e.srcElement;
|
||||||
|
|
||||||
|
var url = $(src).attr('url');
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
reload: true,
|
||||||
|
});
|
||||||
|
}
|
@ -192,4 +192,22 @@ function loadPartTable(table, url, options={}) {
|
|||||||
if (options.buttons) {
|
if (options.buttons) {
|
||||||
linkButtonsToSelection($(table), options.buttons);
|
linkButtonsToSelection($(table), options.buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Button callbacks for part table buttons */
|
||||||
|
|
||||||
|
$("#multi-part-order").click(function() {
|
||||||
|
var selections = $(table).bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
var parts = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
parts.push(item.pk);
|
||||||
|
});
|
||||||
|
|
||||||
|
launchModalForm("/order/purchase-order/order-parts/", {
|
||||||
|
data: {
|
||||||
|
parts: parts,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
@ -237,6 +237,22 @@ function loadStockTable(table, options) {
|
|||||||
$("#multi-item-move").click(function() {
|
$("#multi-item-move").click(function() {
|
||||||
stockAdjustment('move');
|
stockAdjustment('move');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#multi-item-order").click(function() {
|
||||||
|
var selections = $("#stock-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
var stock = [];
|
||||||
|
|
||||||
|
selections.forEach(function(item) {
|
||||||
|
stock.push(item.pk);
|
||||||
|
});
|
||||||
|
|
||||||
|
launchModalForm("/order/purchase-order/order-parts/", {
|
||||||
|
data: {
|
||||||
|
stock: stock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,6 +99,7 @@ InvenTree
|
|||||||
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/order.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
|
||||||
{% block js_load %}
|
{% block js_load %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<a class='hover-icon'>
|
<a class='hover-icon'>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<img class='hover-img-thumb' {% if image %}src='{{ image.url }}'{% else %}src='{% static "img/blank_image.png" %}'{% endif %}>
|
<img class='hover-img-thumb' {% if image %}src='{{ image.url }}'{% else %}src='{% static "img/blank_image.png" %}'{% endif %}>
|
||||||
{% if hover %}
|
{% if hover and image %}
|
||||||
<img class='hover-img-large' {% if image %}src='{{ image.url }}'{% else %}src='{% static "img/blank_image.png" %}'{% endif %}>
|
<img class='hover-img-large' {% if image %}src='{{ image.url }}'{% else %}src='{% static "img/blank_image.png" %}'{% endif %}>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
@ -32,5 +34,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block post_form_content %}
|
{% block post_form_content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -2,19 +2,22 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Part</th>
|
<th>Part</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Required</th>
|
|
||||||
<th>In Stock</th>
|
<th>In Stock</th>
|
||||||
<th>On Order</th>
|
<th>On Order</th>
|
||||||
|
<th>Allocted</th>
|
||||||
<th>Net Stock</th>
|
<th>Net Stock</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for part in parts %}
|
{% for part in parts %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'part-detail' part.id %}">{{ part.full_name }}</a></td>
|
<td>
|
||||||
|
{% include "hover_image.html" with image=part.image hover=True %}
|
||||||
|
<a href="{% url 'part-detail' part.id %}">{{ part.full_name }}</a>
|
||||||
|
</td>
|
||||||
<td>{{ part.description }}</td>
|
<td>{{ part.description }}</td>
|
||||||
<td>{{ part.allocation_count }}</td>
|
|
||||||
<td>{{ part.total_stock }}</td>
|
<td>{{ part.total_stock }}</td>
|
||||||
<td>{{ part.on_order }}</td>
|
<td>{{ part.on_order }}</td>
|
||||||
<td>{{ part.available_stock }}</td>
|
<td>{{ part.allocation_count }}</td>
|
||||||
|
<td{% if part.net_stock < 0 %} class='red-cell'{% endif %}>{{ part.net_stock }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
@ -10,6 +10,7 @@
|
|||||||
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
|
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
|
||||||
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Count stock</a></li>
|
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Count stock</a></li>
|
||||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move stock</a></li>
|
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move stock</a></li>
|
||||||
|
<li><a href='#' id='multi-item-order' title='Order selected items'>Order stock</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,8 @@ ignore =
|
|||||||
# - W293 - blank lines contain whitespace
|
# - W293 - blank lines contain whitespace
|
||||||
W293,
|
W293,
|
||||||
# - E501 - line too long (82 characters)
|
# - E501 - line too long (82 characters)
|
||||||
E501
|
E501,
|
||||||
|
# - C901 - function is too complex
|
||||||
|
C901,
|
||||||
exclude = .git,__pycache__,*/migrations/*
|
exclude = .git,__pycache__,*/migrations/*
|
||||||
max-complexity = 20
|
max-complexity = 20
|
||||||
|
Loading…
Reference in New Issue
Block a user