Merge pull request #489 from SchrodingersGat/export-stocktake

Export stocktake
This commit is contained in:
Oliver 2019-09-09 00:05:05 +10:00 committed by GitHub
commit d8a3c7a81d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 299 additions and 12 deletions

View File

@ -94,6 +94,18 @@ def MakeBarcode(object_type, object_id, object_url, data={}):
return json.dumps(data, sort_keys=True) return json.dumps(data, sort_keys=True)
def GetExportFormats():
""" Return a list of allowable file formats for exporting data """
return [
'csv',
'tsv',
'xls',
'xlsx',
'json',
]
def DownloadFile(data, filename, content_type='application/text'): def DownloadFile(data, filename, content_type='application/text'):
""" Create a dynamic file for the user to download. """ Create a dynamic file for the user to download.

View File

@ -76,12 +76,14 @@ function loadStockTable(table, options) {
} }
else if (field == 'quantity') { else if (field == 'quantity') {
var stock = 0; var stock = 0;
var items = 0;
data.forEach(function(item) { data.forEach(function(item) {
stock += item.quantity; stock += item.quantity;
items += 1;
}); });
return stock; return stock + " (" + items + " items)";
} else if (field == 'batch') { } else if (field == 'batch') {
var batches = []; var batches = [];

View File

@ -10,7 +10,7 @@ class StatusCode:
@classmethod @classmethod
def label(cls, value): def label(cls, value):
""" Return the status code label associated with the provided value """ """ Return the status code label associated with the provided value """
return cls.options.get(value, '') return cls.options.get(value, value)
class OrderStatus(StatusCode): class OrderStatus(StatusCode):

View File

@ -27,4 +27,18 @@
] ]
}); });
$("#stock-export").click(function() {
launchModalForm("{% url 'stock-export-options' %}", {
submit_text: "Export",
success: function(response) {
var url = "{% url 'stock-export' %}";
url += "?format=" + response.format;
url += "&supplier={{ company.id }}";
location.href = url;
},
});
});
{% endblock %} {% endblock %}

View File

@ -247,6 +247,7 @@ class PurchaseOrder(Order):
if line.part: if line.part:
stock = StockItem( stock = StockItem(
part=line.part.part, part=line.part.part,
supplier_part=line.part,
location=location, location=location,
quantity=quantity, quantity=quantity,
purchase_order=self) purchase_order=self)

View File

@ -25,6 +25,27 @@ InvenTree | {{ order }}
{% if order.URL %} {% if order.URL %}
<a href="{{ order.URL }}">{{ order.URL }}</a> <a href="{{ order.URL }}">{{ order.URL }}</a>
{% endif %} {% endif %}
<p>
<div class='btn-row'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' id='edit-order' title='Edit order information'>
<span class='glyphicon glyphicon-edit'></span>
</button>
<button type='button' class='btn btn-default btn-glyph' id='export-order' title='Export order to file'>
<span class='glyphicon glyphicon-download-alt'></span>
</button>
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-default btn-glyph' id='place-order' title='Place order'>
<span class='glyphicon glyphicon-send'></span>
</button>
{% elif order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-default btn-glyph' id='receive-order' title='Receive items'>
<span class='glyphicon glyphicon-check'></span>
</button>
{% endif %}
</div>
</div>
</p>
</div> </div>
</div> </div>
</div> </div>
@ -65,13 +86,6 @@ InvenTree | {{ order }}
{% if order.status == OrderStatus.PENDING %} {% if order.status == OrderStatus.PENDING %}
<button type='button' class='btn btn-default' id='new-po-line'>Add Line Item</button> <button type='button' class='btn btn-default' id='new-po-line'>Add Line Item</button>
{% endif %} {% endif %}
<button type='button' class='btn btn-primary' id='edit-order'>Edit Order</button>
{% if order.status == OrderStatus.PENDING and order.lines.count > 0 %}
<button type='button' class='btn btn-primary' id='place-order'>Place Order</button>
{% elif order.status == OrderStatus.PLACED %}
<button type='button' class='btn btn-primary' id='receive-order'>Receive Items</button>
{% 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>

View File

@ -27,7 +27,7 @@ InvenTree | Purchase Orders
$("#po-create").click(function() { $("#po-create").click(function() {
launchModalForm("{% url 'purchase-order-create' %}", launchModalForm("{% url 'purchase-order-create' %}",
{ {
reload: true, follow: true,
} }
); );
}); });

View File

@ -47,6 +47,21 @@
url: "{% url 'api-stock-list' %}", url: "{% url 'api-stock-list' %}",
}); });
$("#stock-export").click(function() {
launchModalForm("{% url 'stock-export-options' %}", {
submit_text: "Export",
success: function(response) {
var url = "{% url 'stock-export' %}";
url += "?format=" + response.format;
url += "&cascade=" + response.cascade;
url += "&part={{ part.id }}";
location.href = url;
},
});
});
$('#item-create').click(function () { $('#item-create').click(function () {
launchModalForm("{% url 'stock-item-create' %}", { launchModalForm("{% url 'stock-item-create' %}", {
reload: true, reload: true,

View File

@ -9,6 +9,7 @@ from django import forms
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from InvenTree.helpers import GetExportFormats
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from .models import StockLocation, StockItem, StockItemTracking from .models import StockLocation, StockItem, StockItemTracking
@ -96,6 +97,33 @@ class SerializeStockForm(forms.ModelForm):
] ]
class ExportOptionsForm(HelperForm):
""" Form for selecting stock export options """
file_format = forms.ChoiceField(label=_('File Format'), help_text=_('Select output file format'))
include_sublocations = forms.BooleanField(required=False, initial=True, help_text=_("Include stock items in sub locations"))
class Meta:
model = StockLocation
fields = [
'file_format',
'include_sublocations',
]
def get_format_choices(self):
""" File format choices """
choices = [(x, x.upper()) for x in GetExportFormats()]
return choices
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['file_format'].choices = self.get_format_choices()
class AdjustStockForm(forms.ModelForm): class AdjustStockForm(forms.ModelForm):
""" Form for performing simple stock adjustments. """ Form for performing simple stock adjustments.

View File

@ -67,6 +67,24 @@
sessionStorage.removeItem('inventree-show-part-locations'); sessionStorage.removeItem('inventree-show-part-locations');
}); });
$("#stock-export").click(function() {
launchModalForm("{% url 'stock-export-options' %}", {
submit_text: "Export",
success: function(response) {
var url = "{% url 'stock-export' %}";
url += "?format=" + response.format;
url += "&cascade=" + response.cascade;
{% if location %}
url += "&location={{ location.id }}";
{% endif %}
location.href = url;
}
});
});
$('#location-create').click(function () { $('#location-create').click(function () {
launchModalForm("{% url 'stock-location-create' %}", launchModalForm("{% url 'stock-location-create' %}",
{ {

View File

@ -51,6 +51,9 @@ stock_urls = [
url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'), url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
url(r'^export-options/?', views.StockExportOptions.as_view(), name='stock-export-options'),
url(r'^export/?', views.StockExport.as_view(), name='stock-export'),
# Individual stock items # Individual stock items
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)), url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),

View File

@ -18,10 +18,14 @@ from InvenTree.views import AjaxView
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView from InvenTree.views import QRCodeView
from InvenTree.helpers import str2bool from InvenTree.status_codes import StockStatus
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import ExtractSerialNumbers from InvenTree.helpers import ExtractSerialNumbers
from datetime import datetime from datetime import datetime
import tablib
from company.models import Company
from part.models import Part from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking from .models import StockItem, StockLocation, StockItemTracking
@ -31,6 +35,7 @@ from .forms import EditStockItemForm
from .forms import AdjustStockForm from .forms import AdjustStockForm
from .forms import TrackingEntryForm from .forms import TrackingEntryForm
from .forms import SerializeStockForm from .forms import SerializeStockForm
from .forms import ExportOptionsForm
class StockIndex(ListView): class StockIndex(ListView):
@ -119,6 +124,178 @@ class StockLocationQRCode(QRCodeView):
return None return None
class StockExportOptions(AjaxView):
""" Form for selecting StockExport options """
model = StockLocation
ajax_form_title = 'Stock Export Options'
form_class = ExportOptionsForm
def post(self, request, *args, **kwargs):
self.request = request
fmt = request.POST.get('file_format', 'csv').lower()
cascade = str2bool(request.POST.get('include_sublocations', False))
# Format a URL to redirect to
url = reverse('stock-export')
url += '?format=' + fmt
url += '&cascade=' + str(cascade)
data = {
'form_valid': True,
'format': fmt,
'cascade': cascade
}
return self.renderJsonResponse(self.request, self.form_class(), data=data)
def get(self, request, *args, **kwargs):
return self.renderJsonResponse(request, self.form_class())
class StockExport(AjaxView):
""" Export stock data from a particular location.
Returns a file containing stock information for that location.
"""
model = StockItem
def get(self, request, *args, **kwargs):
export_format = request.GET.get('format', 'csv').lower()
# Check if a particular location was specified
loc_id = request.GET.get('location', None)
location = None
if loc_id:
try:
location = StockLocation.objects.get(pk=loc_id)
except (ValueError, StockLocation.DoesNotExist):
pass
# Check if a particular supplier was specified
sup_id = request.GET.get('supplier', None)
supplier = None
if sup_id:
try:
supplier = Company.objects.get(pk=sup_id)
except (ValueError, Company.DoesNotExist):
pass
# Check if a particular part was specified
part_id = request.GET.get('part', None)
part = None
if part_id:
try:
part = Part.objects.get(pk=part_id)
except (ValueError, Part.DoesNotExist):
pass
if export_format not in GetExportFormats():
export_format = 'csv'
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
date=datetime.now().strftime("%d-%b-%Y"),
fmt=export_format
)
if location:
# CHeck if locations should be cascading
cascade = str2bool(request.GET.get('cascade', True))
stock_items = location.get_stock_items(cascade)
else:
cascade = True
stock_items = StockItem.objects.all()
if part:
stock_items = stock_items.filter(part=part)
if supplier:
stock_items = stock_items.filter(supplier_part__supplier=supplier)
# Filter out stock items that are not 'in stock'
stock_items = stock_items.filter(customer=None)
stock_items = stock_items.filter(belongs_to=None)
# Column headers
headers = [
_('Stock ID'),
_('Part ID'),
_('Part'),
_('Supplier Part ID'),
_('Supplier ID'),
_('Supplier'),
_('Location ID'),
_('Location'),
_('Quantity'),
_('Batch'),
_('Serial'),
_('Status'),
_('Notes'),
_('Review Needed'),
_('Last Updated'),
_('Last Stocktake'),
_('Purchase Order ID'),
_('Build ID'),
]
data = tablib.Dataset(headers=headers)
for item in stock_items:
line = []
line.append(item.pk)
line.append(item.part.pk)
line.append(item.part.full_name)
if item.supplier_part:
line.append(item.supplier_part.pk)
line.append(item.supplier_part.supplier.pk)
line.append(item.supplier_part.supplier.name)
else:
line.append('')
line.append('')
line.append('')
if item.location:
line.append(item.location.pk)
line.append(item.location.name)
else:
line.append('')
line.append('')
line.append(item.quantity)
line.append(item.batch)
line.append(item.serial)
line.append(StockStatus.label(item.status))
line.append(item.notes)
line.append(item.review_needed)
line.append(item.updated)
line.append(item.stocktake_date)
if item.purchase_order:
line.append(item.purchase_order.pk)
else:
line.append('')
if item.build:
line.append(item.build.pk)
else:
line.append('')
data.append(line)
filedata = data.export(export_format)
return DownloadFile(filedata, filename)
class StockItemQRCode(QRCodeView): class StockItemQRCode(QRCodeView):
""" View for displaying a QR code for a StockItem object """ """ View for displaying a QR code for a StockItem object """

View File

@ -1,5 +1,6 @@
<div id='button-toolbar'> <div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'> <div class='button-toolbar container-fluid' style='float: right;'>
<button class='btn btn-success' id='stock-export' title='Export Stock Information'>Export</button>
{% if not part or part.is_template == False %} {% if not part or part.is_template == False %}
<button class="btn btn-success" id='item-create'>New Stock Item</button> <button class="btn btn-success" id='item-create'>New Stock Item</button>
{% endif %} {% endif %}

View File

@ -50,7 +50,7 @@ test:
# Run code coverage # Run code coverage
coverage: coverage:
python3 InvenTree/manage.py check python3 InvenTree/manage.py check
coverage run InvenTree/manage.py test build common company order part stock InvenTree coverage run InvenTree/manage.py test build common company order part stock InvenTree
coverage html coverage html
# Install packages required to generate code docs # Install packages required to generate code docs

View File

@ -9,6 +9,7 @@ InvenTree Modules
docs/InvenTree/index docs/InvenTree/index
docs/build/index docs/build/index
docs/common/index
docs/company/index docs/company/index
docs/part/index docs/part/index
docs/order/index docs/order/index
@ -18,6 +19,7 @@ The InvenTree Django ecosystem provides the following 'apps' for core functional
* `InvenTree <docs/InvenTree/index.html>`_ - High level management functions * `InvenTree <docs/InvenTree/index.html>`_ - High level management functions
* `Build <docs/build/index.html>`_ - Part build projects * `Build <docs/build/index.html>`_ - Part build projects
* `Common <docs/common/index.html>`_ - Common modules used by various apps
* `Company <docs/company/index.html>`_ - Company management (suppliers / customers) * `Company <docs/company/index.html>`_ - Company management (suppliers / customers)
* `Part <docs/part/index.html>`_ - Part management * `Part <docs/part/index.html>`_ - Part management
* `Order <docs/order/index.html>`_ - Order management * `Order <docs/order/index.html>`_ - Order management