From 0c6d66e7ced5f1c7843eba4221b08db79e56a021 Mon Sep 17 00:00:00 2001 From: Filipp Lepalaan Date: Thu, 1 Oct 2015 00:31:02 +0300 Subject: Inventory bug fixes and performance enhancements --- servo/forms/product.py | 20 ++++- servo/forms/returns.py | 10 +-- servo/migrations/0033_auto_20150930_0937.py | 100 +++++++++++++++++++++ .../0034_purchaseorderitem_sales_order.py | 19 ++++ servo/migrations/0035_auto_20150930_2323.py | 24 +++++ .../0036_purchaseorderitem_purchase_order_ref.py | 19 ++++ .../0037_purchaseorderitem_user_fullname.py | 19 ++++ servo/models/order.py | 6 +- servo/models/purchases.py | 51 ++++++++++- servo/models/shipments.py | 2 +- servo/templates/orders/reserve_products.html | 4 +- servo/templates/products/get_info.html | 4 +- servo/templates/shipments/list_incoming.html | 23 +++-- servo/views/order.py | 20 +++-- servo/views/purchases.py | 8 +- servo/views/shipments.py | 15 ++-- 16 files changed, 300 insertions(+), 44 deletions(-) create mode 100644 servo/migrations/0033_auto_20150930_0937.py create mode 100644 servo/migrations/0034_purchaseorderitem_sales_order.py create mode 100644 servo/migrations/0035_auto_20150930_2323.py create mode 100644 servo/migrations/0036_purchaseorderitem_purchase_order_ref.py create mode 100644 servo/migrations/0037_purchaseorderitem_user_fullname.py diff --git a/servo/forms/product.py b/servo/forms/product.py index a70d69f..61969f1 100644 --- a/servo/forms/product.py +++ b/servo/forms/product.py @@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError from servo.models import Location, User, TaggedItem from servo.models.purchases import PurchaseOrderItem -from servo.models.product import Product, ProductCategory +from servo.models.product import Product, ProductCategory, Inventory from servo.forms.base import BaseModelForm, DatepickerInput, TextInput @@ -114,7 +114,8 @@ class ProductForm(forms.ModelForm): def clean_code(self): code = self.cleaned_data.get('code') if not re.match(r'^[\w\-/]+$', code): - raise ValidationError(_('Product code %s contains invalid characters') % code) + msg = _('Product code %s contains invalid characters') % code + raise ValidationError(msg) return code @@ -207,3 +208,18 @@ class IncomingSearchForm(forms.Form): label=_('Service order is') ) + +class ReserveProductForm(forms.Form): + """ + Form for reserving products for a given SO + """ + inventory = forms.ModelChoiceField( + queryset=Inventory.objects.none(), + label=_('Inventory') + ) + + def __init__(self, order, *args, **kwargs): + super(ReserveProductForm, self).__init__(*args, **kwargs) + inventory = Inventory.objects.filter(location=order.location, + product__in=order.products.all()) + self.fields['inventory'].queryset = inventory diff --git a/servo/forms/returns.py b/servo/forms/returns.py index 9a011b9..ec339ae 100644 --- a/servo/forms/returns.py +++ b/servo/forms/returns.py @@ -59,19 +59,19 @@ class BulkReturnPartForm(forms.ModelForm): class Meta: model = ServicePart widgets = { - 'box_number': forms.Select(attrs={'class': 'input-small'}), - 'part_number': forms.HiddenInput(), - 'part_title': forms.HiddenInput(), + 'box_number' : forms.Select(attrs={'class': 'input-small'}), + 'part_number' : forms.HiddenInput(), + 'part_title' : forms.HiddenInput(), 'service_order': forms.HiddenInput(), - 'return_order': forms.HiddenInput(), + 'return_order' : forms.HiddenInput(), } exclude = [] def __init__(self, *args, **kwargs): super(BulkReturnPartForm, self).__init__(*args, **kwargs) if 'instance' in kwargs: - box_choices = [(0, 'Individual',)] instance = kwargs['instance'] + box_choices = [(0, 'Individual',)] # @TODO: This seems like a totally unnecessary hack... # Why can't I just pass the number of options directly to the form? part_count = instance.shipment.servicepart_set.all().count() diff --git a/servo/migrations/0033_auto_20150930_0937.py b/servo/migrations/0033_auto_20150930_0937.py new file mode 100644 index 0000000..158501d --- /dev/null +++ b/servo/migrations/0033_auto_20150930_0937.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import servo.lib.shorturl + + +class Migration(migrations.Migration): + + dependencies = [ + ('servo', '0032_auto_20150929_1101'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='code', + field=models.CharField(default=servo.lib.shorturl.from_time, unique=True, max_length=32, verbose_name='Code'), + ), + migrations.AlterField( + model_name='product', + name='component_code', + field=models.CharField(default=b'', max_length=1, verbose_name='Component group', blank=True, choices=[(b'0', b'General'), (b'1', b'Visual'), (b'2', b'Displays'), (b'3', b'Mass Storage'), (b'4', b'Input Devices'), (b'5', b'Boards'), (b'6', b'Power'), (b'7', b'Printer'), (b'8', b'Multi-function Device'), (b'9', b'Communication Devices'), (b'A', b'Share'), (b'B', b'iPhone'), (b'E', b'iPod'), (b'F', b'iPad'), (b'G', b'Beats Products'), (b'W', b'Apple Watch')]), + ), + migrations.AlterField( + model_name='product', + name='device_models', + field=models.ManyToManyField(to='servo.DeviceGroup', verbose_name='Device models', blank=True), + ), + migrations.AlterField( + model_name='product', + name='is_serialized', + field=models.BooleanField(default=False, help_text='Product has a serial number', verbose_name='Is serialized'), + ), + migrations.AlterField( + model_name='product', + name='part_type', + field=models.CharField(default=b'OTHER', max_length=18, verbose_name='Part type', choices=[(b'ADJUSTMENT', 'Adjustment'), (b'MODULE', 'Module'), (b'REPLACEMENT', 'Replacement'), (b'SERVICE', 'Service'), (b'SERVICE CONTRACT', 'Service Contract'), (b'OTHER', 'Other')]), + ), + migrations.AlterField( + model_name='product', + name='photo', + field=models.ImageField(upload_to=b'products', null=True, verbose_name='Photo', blank=True), + ), + migrations.AlterField( + model_name='product', + name='shipping', + field=models.FloatField(default=0, verbose_name='Shipping'), + ), + migrations.AlterField( + model_name='purchaseorder', + name='carrier', + field=models.CharField(max_length=32, verbose_name='Carrier', blank=True), + ), + migrations.AlterField( + model_name='purchaseorder', + name='confirmation', + field=models.CharField(default=b'', max_length=32, verbose_name='Confirmation', blank=True), + ), + migrations.AlterField( + model_name='purchaseorder', + name='days_delivered', + field=models.IntegerField(default=1, verbose_name='Delivery Time', blank=True), + ), + migrations.AlterField( + model_name='purchaseorder', + name='reference', + field=models.CharField(default=b'', max_length=32, verbose_name='Reference', blank=True), + ), + migrations.AlterField( + model_name='purchaseorder', + name='supplier', + field=models.CharField(max_length=32, verbose_name='Supplier', blank=True), + ), + migrations.AlterField( + model_name='purchaseorder', + name='tracking_id', + field=models.CharField(max_length=128, verbose_name='Tracking ID', blank=True), + ), + migrations.AlterField( + model_name='user', + name='locale', + field=models.CharField(default=b'da_DK.UTF-8', help_text='Select which language you want to use Servo in.', max_length=32, verbose_name='Language', choices=[(b'da_DK.UTF-8', 'Danish'), (b'nl_NL.UTF-8', 'Dutch'), (b'en_US.UTF-8', 'English'), (b'et_EE.UTF-8', 'Estonian'), (b'fi_FI.UTF-8', 'Finnish'), (b'sv_SE.UTF-8', 'Swedish')]), + ), + migrations.AlterField( + model_name='user', + name='notify_by_email', + field=models.BooleanField(default=False, help_text='Event notifications will also be emailed to you.', verbose_name='Email notifications'), + ), + migrations.AlterField( + model_name='user', + name='photo', + field=models.ImageField(help_text='Maximum avatar size is 1MB', upload_to=b'avatars', null=True, verbose_name='Photo', blank=True), + ), + migrations.AlterField( + model_name='user', + name='tech_id', + field=models.CharField(default=b'', max_length=16, verbose_name='Tech ID', blank=True), + ), + ] diff --git a/servo/migrations/0034_purchaseorderitem_sales_order.py b/servo/migrations/0034_purchaseorderitem_sales_order.py new file mode 100644 index 0000000..8ac0fa8 --- /dev/null +++ b/servo/migrations/0034_purchaseorderitem_sales_order.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servo', '0033_auto_20150930_0937'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderitem', + name='sales_order', + field=models.ForeignKey(editable=False, to='servo.Order', null=True, verbose_name='Sales Order'), + ), + ] diff --git a/servo/migrations/0035_auto_20150930_2323.py b/servo/migrations/0035_auto_20150930_2323.py new file mode 100644 index 0000000..bf4cd40 --- /dev/null +++ b/servo/migrations/0035_auto_20150930_2323.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servo', '0034_purchaseorderitem_sales_order'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderitem', + name='sales_order_ref', + field=models.CharField(default=b'', max_length=8, editable=False), + ), + migrations.AlterField( + model_name='purchaseorderitem', + name='sales_order', + field=models.ForeignKey(editable=False, to='servo.Order', null=True), + ), + ] diff --git a/servo/migrations/0036_purchaseorderitem_purchase_order_ref.py b/servo/migrations/0036_purchaseorderitem_purchase_order_ref.py new file mode 100644 index 0000000..f8d3cc7 --- /dev/null +++ b/servo/migrations/0036_purchaseorderitem_purchase_order_ref.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servo', '0035_auto_20150930_2323'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderitem', + name='purchase_order_ref', + field=models.CharField(default=b'', max_length=32, editable=False), + ), + ] diff --git a/servo/migrations/0037_purchaseorderitem_user_fullname.py b/servo/migrations/0037_purchaseorderitem_user_fullname.py new file mode 100644 index 0000000..0e5993f --- /dev/null +++ b/servo/migrations/0037_purchaseorderitem_user_fullname.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servo', '0036_purchaseorderitem_purchase_order_ref'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderitem', + name='user_fullname', + field=models.CharField(default=b'', max_length=256, editable=False), + ), + ] diff --git a/servo/models/order.py b/servo/models/order.py index 18fc3b0..b389053 100644 --- a/servo/models/order.py +++ b/servo/models/order.py @@ -931,8 +931,12 @@ class ServiceOrderItem(AbstractOrderItem): return self.order.repair_set.latest() def reserve_product(self): + """ + Reserve this SOI for the inventory at this location + """ location = self.order.location - inventory = Inventory.objects.get(location=location, product=self.product) + inventory, created = Inventory.objects.get_or_create(location=location, + product=self.product) inventory.amount_reserved += self.amount inventory.save() diff --git a/servo/models/purchases.py b/servo/models/purchases.py index 184f303..636eab6 100644 --- a/servo/models/purchases.py +++ b/servo/models/purchases.py @@ -188,7 +188,9 @@ class PurchaseOrder(models.Model): class PurchaseOrderItem(AbstractOrderItem): - "An item being purchased" + """ + An item being purchased + """ price = models.DecimalField( max_digits=8, decimal_places=2, @@ -202,8 +204,34 @@ class PurchaseOrderItem(AbstractOrderItem): verbose_name=_("Purchase Order") ) + # begin optimization + sales_order = models.ForeignKey( + Order, + null=True, + editable=False, + ) + + sales_order_ref = models.CharField( + default='', + max_length=8, + editable=False, + ) + + purchase_order_ref = models.CharField( + default='', + max_length=32, + editable=False, + ) + + user_fullname = models.CharField( + default='', + max_length=256, + editable=False, + ) + # /end optimization + order_item = models.ForeignKey(ServiceOrderItem, null=True, editable=False) - reference = models.CharField(default='', blank=True, max_length=128) + reference = models.CharField(default='', blank=True, max_length=128) ordered_at = models.DateTimeField(null=True, editable=False) expected_ship_date = models.DateField(null=True, editable=False) @@ -255,7 +283,23 @@ class PurchaseOrderItem(AbstractOrderItem): self.save() def save(self, *args, **kwargs): + + # The following four fields are used so much + # that we store them for fast access + if self.sales_order is None: + self.sales_order = self.purchase_order.sales_order + + if self.sales_order_ref == '': + self.sales_order_ref = self.sales_order.code + + if self.purchase_order_ref == '': + self.purchase_order_ref = self.purchase_order.reference + + if self.user_fullname == '': + self.user_fullname = self.created_by.get_name() + super(PurchaseOrderItem, self).save(*args, **kwargs) + # Sync SOI and POI serial numbers if self.order_item: if self.order_item.sn and not self.sn: @@ -265,6 +309,9 @@ class PurchaseOrderItem(AbstractOrderItem): self.order_item.save() + def __unicode__(self): + return self.code + class Meta: ordering = ('id',) app_label = 'servo' diff --git a/servo/models/shipments.py b/servo/models/shipments.py index d6112ab..614c642 100644 --- a/servo/models/shipments.py +++ b/servo/models/shipments.py @@ -62,7 +62,7 @@ class Shipment(models.Model): default='', max_length=18, choices=gsxws.CARRIERS, - verbose_name=_('carrier') + verbose_name=_('Carrier') ) created_at = models.DateTimeField(auto_now=True, editable=False) diff --git a/servo/templates/orders/reserve_products.html b/servo/templates/orders/reserve_products.html index 318a28d..08640b5 100755 --- a/servo/templates/orders/reserve_products.html +++ b/servo/templates/orders/reserve_products.html @@ -2,11 +2,11 @@ {% load i18n %} {% block header %} - {% blocktrans with id=order.code %}Reserve all products in order {{ id }}?{% endblocktrans %} + {% blocktrans with id=order.code %}Reserve products in order {{ id }}{% endblocktrans %} {% endblock header %} {% block footer %} -
+ {% csrf_token %}
diff --git a/servo/templates/products/get_info.html b/servo/templates/products/get_info.html index be024e2..262d1e8 100755 --- a/servo/templates/products/get_info.html +++ b/servo/templates/products/get_info.html @@ -38,7 +38,7 @@ {% endifchanged %}
{% trans "Stocked" %}
-
{{ i.amount_stocked }}
+
{{ i.amount_stocked|default:"-" }}
{% trans "Ordered" %}
{{ i.amount_ordered|default:"-" }}
{% trans "Reserved" %}
@@ -56,4 +56,4 @@ {% trans "Edit" %} {% endif %} -{% endblock footer %} \ No newline at end of file +{% endblock footer %} diff --git a/servo/templates/shipments/list_incoming.html b/servo/templates/shipments/list_incoming.html index 3c9da41..96289aa 100755 --- a/servo/templates/shipments/list_incoming.html +++ b/servo/templates/shipments/list_incoming.html @@ -1,8 +1,7 @@ {% extends "shipments/index.html" %} {% load i18n %} -{% block toolbar %} -{% endblock toolbar %} +{% block toolbar %}{% endblock toolbar %} {% block second_column %} {% include "snippets/filtering_form.html" %} @@ -26,24 +25,24 @@ {% if can_receive %} {% endif %} - {% with i.product as p %} - - {{ p.code }}
{{ p.title }} + + {{ i.code }}
{{ i.title }} - {% endwith %} {% with i.purchase_order as po %} - - {% if po.sales_order %} - {{ po.sales_order.code }} + + {% if i.sales_order %} + {{ i.sales_order_ref }} {% endif %} -
{{ po.reference }} +
{{ i.purchase_order_ref }} {{ po.confirmation }} - {{ po.created_by }}
{{ po.submitted_at|date:"SHORT_DATE_FORMAT" }} + {{ i.user_fullname }}
{{ i.created_at|date:"SHORT_DATE_FORMAT" }} {% endwith %} {% empty %} - {% trans "No incoming products" %} + + {% trans "No incoming products" %} + {% endfor %} diff --git a/servo/views/order.py b/servo/views/order.py index 1712ba0..cc6d430 100644 --- a/servo/views/order.py +++ b/servo/views/order.py @@ -310,7 +310,8 @@ def toggle_task(request, order_id, item_id): checklist_item = get_object_or_404(ChecklistItem, pk=item_id) try: - item = ChecklistItemValue.objects.get(order_id=order_id, item=checklist_item) + item = ChecklistItemValue.objects.get(order_id=order_id, + item=checklist_item) item.delete() except ChecklistItemValue.DoesNotExist: item = ChecklistItemValue() @@ -348,6 +349,7 @@ def repair(request, order_id, repair_id): @permission_required("servo.change_order") def complete_repair(request, order_id, repair_id): repair = get_object_or_404(Repair, pk=repair_id) + if request.method == 'POST': try: repair.close(request.user) @@ -414,7 +416,8 @@ def delete(request, pk): return redirect(return_to) except Exception as e: ed = {'order': order.code, 'error': e} - messages.error(request, _(u'Cannot delete order %(order)s: %(error)s') % ed) + msg = _(u'Cannot delete order %(order)s: %(error)s') % ed + messages.error(request, msg) return redirect(order) action = request.path @@ -450,13 +453,14 @@ def remove_user(request, pk, user_id): Removes this user from the follower list, unsets assignee """ order = get_object_or_404(Order, pk=pk) - user = get_object_or_404(User, pk=user_id) + user = get_object_or_404(User, pk=user_id) try: order.remove_follower(user) if user == order.user: order.set_user(None, request.user) - order.notify("unset_user", _('User %s removed from followers') % user, request.user) + msg = _('User %s removed from followers') % user + order.notify("unset_user", msg, request.user) except Exception as e: messages.error(request, e) @@ -623,7 +627,7 @@ def device_from_product(request, pk, item_id): """ Turns a SOI into a device and attaches it to this order """ - order = Order.objects.get(pk=pk) + order = get_object_or_404(Order, pk=pk) soi = ServiceOrderItem.objects.get(pk=item_id) try: @@ -640,8 +644,7 @@ def device_from_product(request, pk, item_id): @permission_required('servo.change_order') def reserve_products(request, pk): - order = Order.objects.get(pk=pk) - location = request.user.get_location() + order = get_object_or_404(Order, pk=pk) if request.method == 'POST': for p in order.products.all(): @@ -653,8 +656,7 @@ def reserve_products(request, pk): return redirect(order) - data = {'order': order, 'action': request.path} - return render(request, "orders/reserve_products.html", data) + return render(request, "orders/reserve_products.html", locals()) @permission_required("servo.change_order") diff --git a/servo/views/purchases.py b/servo/views/purchases.py index 7233ceb..4b789ee 100644 --- a/servo/views/purchases.py +++ b/servo/views/purchases.py @@ -14,7 +14,8 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib import messages from servo.models.order import ServiceOrderItem -from servo.models import Product, GsxAccount, PurchaseOrder, PurchaseOrderItem +from servo.models import (Order, Product, GsxAccount, + PurchaseOrder, PurchaseOrderItem,) from servo.forms import PurchaseOrderItemEditForm, PurchaseOrderSearchForm @@ -202,12 +203,15 @@ def delete_po(request, po_id): @permission_required('servo.add_purchaseorder') def create_po(request, product_id=None, order_id=None): + """ + Creates a new Purchase Order + """ po = PurchaseOrder(created_by=request.user) po.location = request.user.get_location() po.save() if order_id is not None: - po.sales_order_id = order_id + po.sales_order = Order.objects.get(pk=order_id) po.save() for i in ServiceOrderItem.objects.filter(order_id=order_id): po.add_product(i, amount=1, user=request.user) diff --git a/servo/views/shipments.py b/servo/views/shipments.py index ada3a07..18c44ba 100644 --- a/servo/views/shipments.py +++ b/servo/views/shipments.py @@ -25,7 +25,9 @@ def prep_counts(): def prep_list_view(request): - + """ + Prepares the list view for incoming parts and products + """ from datetime import timedelta now = timezone.now() @@ -33,7 +35,7 @@ def prep_list_view(request): data['counts'] = prep_counts() location = request.user.get_location() - ordered_date_range = [now - timedelta(days=30), timezone.now()] + ordered_date_range = [now - timedelta(days=30), timezone.now()] received_date_range = [now - timedelta(days=30), timezone.now()] initial = { @@ -47,7 +49,7 @@ def prep_list_view(request): else: data['form'] = IncomingSearchForm(initial=initial) - inventory = PurchaseOrderItem.objects.filter(received_at=None) + inventory = PurchaseOrderItem.objects.filter(received_at=None).select_related('purchase_order', 'sales_order') inventory = inventory.exclude(purchase_order__submitted_at=None) if request.method == 'POST': @@ -106,7 +108,7 @@ def list_incoming(request, shipment=None, status=""): item = PurchaseOrderItem.objects.get(pk=i) try: item.receive(request.user) - except ValueError, e: + except ValueError as e: messages.error(request, e) return redirect(list_incoming) @@ -228,16 +230,16 @@ def edit_bulk_return(request, pk=None, ship_to=None): return redirect(edit_bulk_return, ship_to=ship_to) shipment = Shipment.get_current(request.user, location, ship_to) - part_count = shipment.servicepart_set.all().count() PartFormSet = inlineformset_factory(Shipment, ServicePart, form=BulkReturnPartForm, extra=0, exclude=[]) + form = BulkReturnForm(instance=shipment) formset = PartFormSet(instance=shipment) - + if request.method == "POST": form = BulkReturnForm(request.POST, instance=shipment) if form.is_valid(): @@ -246,6 +248,7 @@ def edit_bulk_return(request, pk=None, ship_to=None): shipment = form.save() msg = _("Bulk return saved") formset.save() + if "confirm" in request.POST.keys(): try: shipment.register_bulk_return(request.user) -- cgit v1.2.3