diff options
Diffstat (limited to 'servo')
-rw-r--r-- | servo/forms/product.py | 20 | ||||
-rw-r--r-- | servo/forms/returns.py | 10 | ||||
-rw-r--r-- | servo/migrations/0033_auto_20150930_0937.py | 100 | ||||
-rw-r--r-- | servo/migrations/0034_purchaseorderitem_sales_order.py | 19 | ||||
-rw-r--r-- | servo/migrations/0035_auto_20150930_2323.py | 24 | ||||
-rw-r--r-- | servo/migrations/0036_purchaseorderitem_purchase_order_ref.py | 19 | ||||
-rw-r--r-- | servo/migrations/0037_purchaseorderitem_user_fullname.py | 19 | ||||
-rw-r--r-- | servo/models/order.py | 6 | ||||
-rw-r--r-- | servo/models/purchases.py | 51 | ||||
-rw-r--r-- | servo/models/shipments.py | 2 | ||||
-rwxr-xr-x | servo/templates/orders/reserve_products.html | 4 | ||||
-rwxr-xr-x | servo/templates/products/get_info.html | 4 | ||||
-rwxr-xr-x | servo/templates/shipments/list_incoming.html | 23 | ||||
-rw-r--r-- | servo/views/order.py | 20 | ||||
-rw-r--r-- | servo/views/purchases.py | 8 | ||||
-rw-r--r-- | servo/views/shipments.py | 15 |
16 files changed, 300 insertions, 44 deletions
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 %} -<form action="{{ action }}" method="post" accept-charset="utf-8"> +<form action="{{ request.path }}" method="post" accept-charset="utf-8"> {% csrf_token %} <button class="btn btn-primary" type="submit">{% trans "Reserve" %}</button> </form> 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 %} <dl class="dl-horizontal"> <dt>{% trans "Stocked" %}</dt> - <dd>{{ i.amount_stocked }}</dd> + <dd>{{ i.amount_stocked|default:"-" }}</dd> <dt>{% trans "Ordered" %}</dt> <dd>{{ i.amount_ordered|default:"-" }}</dd> <dt>{% trans "Reserved" %}</dt> @@ -56,4 +56,4 @@ <a class="btn btn-default" href="{% url 'products-edit_product' pk=product.pk group='all' %}">{% trans "Edit" %}</a> {% endif %} <button type="submit" class="btn btn-primary" data-dismiss="modal">{% trans "Done" %}</button> -{% 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 %} <td><input type="checkbox" name="id" value="{{ i.pk|safe }}" class="toggle-submit"/></td> {% endif %} - {% with i.product as p %} - <td data-value="{{ p.code }}"> - <strong><a href="{% url 'shipments-view_incoming' i.pk %}" data-modal="#modal">{{ p.code }}</a></strong><br/>{{ p.title }} + <td data-value="{{ i.code }}"> + <strong><a href="{% url 'shipments-view_incoming' i.pk %}" data-modal="#modal">{{ i.code }}</a></strong><br/>{{ i.title }} </td> - {% endwith %} {% with i.purchase_order as po %} - <td data-value="{{ po.sales_order.code }}"> - {% if po.sales_order %} - <a href="{% url 'orders-edit' po.sales_order.pk %}">{{ po.sales_order.code }}</a> + <td data-value="{{ i.sales_order_ref }}"> + {% if i.sales_order %} + <a href="{% url 'orders-edit' i.sales_order_id %}">{{ i.sales_order_ref }}</a> {% endif %} - <br/><small class="muted">{{ po.reference }}</small> + <br/><small class="muted">{{ i.purchase_order_ref }}</small> </td> <td>{{ po.confirmation }}</td> - <td>{{ po.created_by }}<br/><small class="muted">{{ po.submitted_at|date:"SHORT_DATE_FORMAT" }}</small></td> + <td>{{ i.user_fullname }}<br/><small class="muted">{{ i.created_at|date:"SHORT_DATE_FORMAT" }}</small></td> {% endwith %} </tr> {% empty %} - <tr><td colspan="7" class="muted empty">{% trans "No incoming products" %}</td></tr> + <tr> + <td colspan="7" class="muted empty">{% trans "No incoming products" %}</td> + </tr> {% endfor %} </tbody> </table> 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) |