aboutsummaryrefslogtreecommitdiffstats
path: root/servo/models/purchases.py
diff options
context:
space:
mode:
Diffstat (limited to 'servo/models/purchases.py')
-rw-r--r--servo/models/purchases.py353
1 files changed, 353 insertions, 0 deletions
diff --git a/servo/models/purchases.py b/servo/models/purchases.py
new file mode 100644
index 0000000..4b9a886
--- /dev/null
+++ b/servo/models/purchases.py
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, First Party Software
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+from django.db import models
+from django.conf import settings
+from django.utils import timezone
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+
+from django.dispatch import receiver
+from django.db.models.signals import post_save
+
+from servo import defaults
+from servo.models.common import Location, Configuration
+from servo.models.product import Product, Inventory
+from servo.models.order import Order, AbstractOrderItem, ServiceOrderItem
+
+
+class PurchaseOrder(models.Model):
+ """
+ A purchase order(PO) consists of different purchase order items
+ all of which may reference individual Service Orders.
+ When a PO is submitted, the included items are registered
+ to the /products/incoming/ list (items that have not yet arrived).
+ A PO cannot be edited after it's been submitted.
+
+ Creating a PO from an SO only creates the PO, it does not submit it.
+ """
+ site = models.ForeignKey(
+ Site,
+ editable=False,
+ default=defaults.site_id
+ )
+ location = models.ForeignKey(
+ Location,
+ editable=False,
+ help_text=_('The location from which this PO was created')
+ )
+ sales_order = models.ForeignKey(Order, null=True, editable=False)
+ reference = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_("reference"),
+ )
+ confirmation = models.CharField(
+ blank=True,
+ default='',
+ max_length=32,
+ verbose_name=_("confirmation"),
+ )
+
+ created_by = models.ForeignKey(settings.AUTH_USER_MODEL, editable=False)
+ created_at = models.DateTimeField(auto_now_add=True, editable=False)
+ submitted_at = models.DateTimeField(null=True, editable=False)
+
+ supplier = models.CharField(
+ blank=True,
+ max_length=32,
+ verbose_name=_("supplier")
+ )
+ carrier = models.CharField(
+ blank=True,
+ max_length=32,
+ verbose_name=_("carrier")
+ )
+ tracking_id = models.CharField(
+ blank=True,
+ max_length=128,
+ verbose_name=_("tracking ID")
+ )
+ days_delivered = models.IntegerField(
+ blank=True,
+ default=1,
+ verbose_name=_("delivery Time")
+ )
+
+ has_arrived = models.BooleanField(default=False)
+ total = models.FloatField(null=True, editable=False)
+ invoice_id = models.CharField(default='', max_length=10, editable=False)
+ invoice = models.FileField(
+ null=True,
+ editable=False,
+ upload_to="gsx_invoices",
+ help_text="Apple's sales invoice for this PO"
+ )
+
+ def only_apple_parts(self):
+ for p in self.purchaseorderitem_set.all():
+ if not p.product.is_apple_part:
+ return False
+ return True
+
+ @property
+ def is_editable(self):
+ return self.submitted_at is None
+
+ def can_create_gsx_stock(self):
+ return self.is_editable and self.confirmation == ''
+
+ def order_items(self, items):
+ pass
+
+ def get_absolute_url(self):
+ from django.core.urlresolvers import reverse
+ if self.submitted_at:
+ return reverse("purchases-view_po", args=[self.pk])
+ return reverse("purchases-edit_po", args=[self.pk])
+
+ def sum(self):
+ total = 0
+ for p in self.purchaseorderitem_set.all():
+ total += float(p.price*p.amount)
+
+ return total
+
+ def amount(self):
+ amount = 0
+ for p in self.purchaseorderitem_set.all():
+ amount += p.amount
+
+ return amount
+
+ def submit(self, user):
+ "Submits this Purchase Order"
+ if self.submitted_at is not None:
+ raise ValueError(_("Purchase Order %d has already been submitted") % self.pk)
+
+ location = user.get_location()
+
+ for i in self.purchaseorderitem_set.all():
+ inventory = Inventory.objects.get_or_create(location=location,
+ product=i.product)[0]
+ inventory.amount_ordered += i.amount
+ inventory.save()
+ i.ordered_at = timezone.now()
+ i.save()
+
+ self.submitted_at = timezone.now()
+ self.save()
+
+ def cancel(self):
+ """
+ Cancels this Purchase Order
+ Declined Repairs etc
+ """
+ location = self.created_by.get_location()
+
+ for i in self.purchaseorderitem_set.all():
+ inventory = Inventory.objects.get(location=location, product=i.product)
+ inventory.amount_ordered -= i.amount
+ inventory.save()
+ i.expected_ship_date = None
+ i.save()
+
+ def add_product(self, product, amount, user):
+ """
+ Adds a product to this Purchase Order
+ """
+ poi = PurchaseOrderItem(amount=amount, purchase_order=self)
+ poi.created_by = user
+ # adding from a Service Order
+ if isinstance(product, AbstractOrderItem):
+ poi.code = product.product.code
+ poi.order_item = product
+ poi.price = product.price
+ poi.product_id = product.product.id
+ poi.title = product.product.title
+ # adding from Stock
+ if isinstance(product, Product):
+ poi.code = product.code
+ poi.title = product.title
+ poi.product_id = product.id
+ poi.price = product.price_purchase_stock
+
+ poi.save()
+
+ def delete(self, *args, **kwargs):
+ if self.submitted_at:
+ raise ValueError(_('Submitted orders cannot be deleted'))
+ return super(PurchaseOrder, self).delete(*args, **kwargs)
+
+ class Meta:
+ ordering = ('-id',)
+ app_label = 'servo'
+
+
+class PurchaseOrderItem(AbstractOrderItem):
+ "An item being purchased"
+ price = models.DecimalField(
+ max_digits=8,
+ decimal_places=2,
+ verbose_name=_("Purchase Price"),
+ help_text=_("Purchase price without taxes")
+ )
+
+ purchase_order = models.ForeignKey(
+ PurchaseOrder,
+ editable=False,
+ verbose_name=_("Purchase Order")
+ )
+
+ order_item = models.ForeignKey(ServiceOrderItem, null=True, editable=False)
+ 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)
+ received_at = models.DateTimeField(
+ null=True,
+ blank=True,
+ editable=False,
+ verbose_name=_("arrived")
+ )
+
+ received_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ null=True,
+ editable=False,
+ related_name='+'
+ )
+
+ @classmethod
+ def from_soi(cls, po, soi, user):
+ """
+ Creates a new POI from a Sales Order item
+ """
+ poi = cls(purchase_order=po, order_item=soi)
+ poi.code = soi.code
+ poi.title = soi.title
+ poi.created_by = user
+
+ poi.price = soi.get_purchase_price()
+ poi.product = soi.product
+
+ return poi
+
+ def get_incoming_url(self):
+ """
+ Returns the correct URL to receive this item
+ """
+ if self.received_at is None:
+ date = "0000-00-00"
+ else:
+ date = self.received_at.strftime("%Y-%m-%d")
+
+ return "/sales/shipments/incoming/%s/%d/" % (date, self.pk)
+
+ def receive(self, user):
+ if self.received_at is not None:
+ raise ValueError(_("Product has already been received"))
+ self.received_at = timezone.now()
+ self.received_by = user
+ self.save()
+
+ def save(self, *args, **kwargs):
+ 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:
+ self.sn = self.order_item.sn
+ else:
+ self.order_item.sn = self.sn
+
+ self.order_item.save()
+
+ class Meta:
+ ordering = ('id',)
+ app_label = 'servo'
+ get_latest_by = 'id'
+
+
+@receiver(post_save, sender=PurchaseOrderItem)
+def trigger_product_received(sender, instance, created, **kwargs):
+
+ if instance.received_at is None:
+ return
+
+ product = instance.product
+ po = instance.purchase_order
+ location = po.created_by.get_location()
+
+ inventory = Inventory.objects.get_or_create(location=location, product=product)[0]
+
+ # Receiving an incoming item
+ if Configuration.track_inventory():
+ try:
+ inventory.amount_ordered -= instance.amount
+ inventory.amount_stocked += instance.amount
+ inventory.save()
+ except Exception:
+ ref = po.reference or po.confirmation
+ ed = {'prod': product.code, 'ref': ref}
+ raise ValueError(_('Cannot receive item %(prod)s (%(ref)s)') % ed)
+
+ sales_order = instance.purchase_order.sales_order
+
+ if sales_order is None:
+ return
+
+ # Trigger status change for parts receive
+ if sales_order.queue:
+ new_status = sales_order.queue.status_products_received
+ if new_status and sales_order.is_editable:
+ user = instance.received_by or instance.created_by
+ sales_order.set_status(new_status, user)
+
+
+@receiver(post_save, sender=PurchaseOrder)
+def trigger_purchase_order_created(sender, instance, created, **kwargs):
+
+ sales_order = instance.sales_order
+
+ if sales_order is None:
+ return
+
+ if not sales_order.is_editable:
+ return
+
+ if created:
+ msg = _("Purchase Order %d created") % instance.id
+ sales_order.notify("po_created", msg, instance.created_by)
+
+ # Trigger status change for GSX repair submit (if defined)
+ if instance.submitted_at:
+ if sales_order.queue:
+ queue = sales_order.queue
+ if queue.status_products_ordered:
+ # Queue has a status for product_ordered - trigger it
+ new_status = queue.status_products_ordered
+ sales_order.set_status(new_status, instance.created_by)