From 63b0fc6269b38edf7234b9f151b80d81f614c0a3 Mon Sep 17 00:00:00 2001 From: Filipp Lepalaan Date: Tue, 4 Aug 2015 10:11:24 +0300 Subject: Initial commit First public commit --- servo/models/repair.py | 641 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 servo/models/repair.py (limited to 'servo/models/repair.py') diff --git a/servo/models/repair.py b/servo/models/repair.py new file mode 100644 index 0000000..c45db2f --- /dev/null +++ b/servo/models/repair.py @@ -0,0 +1,641 @@ +# -*- 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. + +import json +import gsxws +import os.path + +from django.db import models +from django.conf import settings +from django.utils import timezone +from django.core.urlresolvers import reverse +from django.contrib.sites.models import Site +from django.utils.translation import ugettext_lazy as _ +from django.core.validators import MaxLengthValidator +from django.contrib.sites.managers import CurrentSiteManager + +from servo import defaults +from servo.models.common import GsxAccount +from servo.models import Queue, Order, Device, Product +from servo.models.order import ServiceOrderItem +from servo.models.parts import ServicePart +from servo.models.purchases import PurchaseOrder, PurchaseOrderItem + + +class Checklist(models.Model): + site = models.ForeignKey( + Site, + editable=False, + default=defaults.site_id + ) + + title = models.CharField( + max_length=255, + unique=True, + verbose_name=_("title"), + default=_('New Checklist') + ) + queues = models.ManyToManyField( + Queue, + blank=True, + verbose_name=_("queue") + ) + + enabled = models.BooleanField(default=True, verbose_name=_("Enabled")) + objects = CurrentSiteManager() + + def get_admin_url(self): + return reverse('admin-edit_checklist', args=[self.pk]) + + def __unicode__(self): + return self.title + + class Meta: + app_label = "servo" + ordering = ("title",) + verbose_name = _('Checklist') + verbose_name_plural = _('Checklists') + + +class ChecklistItem(models.Model): + checklist = models.ForeignKey(Checklist) + title = models.CharField(max_length=255, verbose_name=_("Task")) + description = models.TextField( + blank=True, + default='', + verbose_name=_('Description') + ) + """ + reported = models.BooleanField( + default=True, + verbose_name=_("Reported"), + help_text=_('Report this result to the customer') + ) + """ + + def __unicode__(self): + return self.title + + class Meta: + app_label = "servo" + + +class ChecklistItemValue(models.Model): + order = models.ForeignKey(Order) + item = models.ForeignKey(ChecklistItem) + + checked_at = models.DateTimeField(auto_now_add=True) + checked_by = models.ForeignKey(settings.AUTH_USER_MODEL) + + class Meta: + app_label = "servo" + + +class Repair(models.Model): + """ + Proxies service order data between our internal + service orders and GSX repairs + """ + order = models.ForeignKey(Order, editable=False, on_delete=models.PROTECT) + device = models.ForeignKey(Device, editable=False, on_delete=models.PROTECT) + parts = models.ManyToManyField(ServiceOrderItem, through=ServicePart) + + created_at = models.DateTimeField(auto_now_add=True, editable=False) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + on_delete=models.PROTECT, + related_name="created_repairs" + ) + + tech_id = models.CharField( + default='', + blank=True, + max_length=15, + verbose_name=_(u'Technician') + ) + unit_received_at = models.DateTimeField( + default=timezone.now, + verbose_name=_(u'Unit Received') + ) + submitted_at = models.DateTimeField(null=True, editable=False) + completed_at = models.DateTimeField(null=True, editable=False) + completed_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + editable=False, + on_delete=models.PROTECT, + related_name="completed_repairs" + ) + request_review = models.BooleanField( + default=False, + help_text=_("Repair should be reviewed by Apple before confirmation") + ) + confirmation = models.CharField(max_length=10, default='', editable=False) + reference = models.CharField( + blank=True, + default='', + max_length=16, + verbose_name=_("Reference") + ) + + symptom = models.TextField() + diagnosis = models.TextField() + notes = models.TextField( + blank=True, + default='', + validators=[MaxLengthValidator(800)], + help_text=_("Notes are mandatory when requesting review.") + ) + status = models.CharField(default='', editable=False, max_length=128) + attachment = models.FileField( + upload_to='repairs', + null=True, + blank=True, + help_text=_('Choose files to be sent with the repair creation request') + ) + repair_number = models.CharField(default='', max_length=12, editable=False) + mark_complete = models.BooleanField( + blank=True, + default=False, + verbose_name=_("mark complete"), + help_text=_("Requires replacement serial number") + ) + replacement_sn = models.CharField( + blank=True, + default='', + max_length=18, + verbose_name=_("New serial number"), + help_text=_("Serial Number of replacement part") + ) + # the account through which this repair was submitted + gsx_account = models.ForeignKey( + GsxAccount, + editable=False, + on_delete=models.PROTECT + ) + + repair_type = models.CharField( + max_length=2, + default="CA", + editable=False, + choices=gsxws.REPAIR_TYPES + ) + + component_data = models.TextField(default='', editable=False) + consumer_law = models.NullBooleanField( + default=None, + help_text=_('Unit is eligible for consumer law coverage') + ) + + @property + def has_cl_parts(self): + """ + Returns true if this repair contains Consumer Law parts + """ + return self.servicepart_set.filter(coverage_status='VW').exists() + + @property + def can_mark_complete(self): + """ + Returns true if this repair can be marked complete after submitting + """ + parts = self.servicepart_set.all() + if len(parts) > 1: return False + replacements = [p for p in parts if p.is_replacement()] + return len(replacements) == 1 + + @classmethod + def create_from_gsx(cls, order, confirmation): + """ + Creates a new Repair for order with confirmation number + """ + try: + repair = cls.objects.get(confirmation=confirmation) + msg = {'repair': repair.confirmation, 'order': repair.order} + raise ValueError(_('Repair %(repair)s already exists for order %(order)s') % msg) + except cls.DoesNotExist: + pass + + repair = cls(order=order) + repair.confirmation=confirmation + repair.save() + repair.update_details() + repair.update_status() + return repair + + def create_purchase_order(self): + # Create local purchase order + po = PurchaseOrder(supplier="Apple", created_by=self.created_by) + po.location = self.created_by.get_location() + po.reference = self.reference + po.sales_order = self.order + po.save() + return po + + def warranty_status(self): + """ + Gets warranty status for this device and these parts + """ + self.connect_gsx(self.created_by) + product = gsxws.Product(self.device.sn) + parts = [(p.code, p.comptia_code,) for p in self.order.get_parts()] + return product.warranty(parts, self.get_received_date()) + + def is_open(self): + return self.completed_at is None + + def get_products(self): + """ + Returns the Service Order Items in this Repair + """ + return [x.order_item for x in self.servicepart_set.all()] + + def get_number(self, user=None): + return self.confirmation or _("New GSX Repair") + + def set_parts(self, parts): + ServicePart.objects.filter(repair=self).delete() + for p in parts: + part = ServicePart.from_soi(self, p) + part.save() + + def add_part(self, order_item, user): + """ + Adds this Order Item as a part to this GSX repair + """ + self.connect_gsx(user) + gsx_rep = self.get_gsx_repair() + + part = ServicePart.from_soi(self, order_item) + order_line = part.get_repair_order_line() + + gsx_rep.update({'orderLines': [order_line]}) + part.order(user) + + return part + + def add_gsx_part(self, part): + """ + Adds a part that has been added manually in GSX web UI + """ + # part has been added to the order, but not the GSX repair + try: + oi = self.order.products.get(code=part.partNumber) + except ServiceOrderItem.DoesNotExist: + new_part = ServicePart(part_number=part.partNumber) + try: + p = Product.objects.get(code=part.partNumber) + except Product.DoesNotExist: + p = Product.from_gsx(new_part.lookup()) + p.save() + + oi = self.order.add_product(p, 1, self.created_by) + + oi.comptia_code = part.comptiaCode or '' + oi.comptia_modifier = part.comptiaModifier or '' + oi.save() + + sp = ServicePart.from_soi(self, oi) + sp.set_part_details(part) + + sp.order(self.created_by) + sp.save() + + def submit(self, customer_data): + """ + Creates a new GSX repair and all the documentation that goes along with it + """ + if len(self.parts.all()) < 1: + raise ValueError(_("Please add some parts to the repair")) + + if not self.order.queue: + raise ValueError(_("Order has not been assigned to a queue")) + + + repair_data = self.to_gsx() + + if self.repair_type == "CA": + gsx_repair = gsxws.CarryInRepair(**repair_data) + if self.repair_type == "ON": + gsx_repair = gsxws.IndirectOnsiteRepair(**repair_data) + + customer_data['regionCode'] = self.gsx_account.region + gsx_repair.customerAddress = gsxws.Customer(**customer_data) + + if self.component_data: + ccd = [] + cd = json.loads(self.component_data) + for k, v in cd.items(): + ccd.append(gsxws.ComponentCheck(component=k, serialNumber=v)) + + gsx_repair.componentCheckDetails = ccd + + parts = [p.get_repair_order_line() for p in self.servicepart_set.all()] + gsx_repair.orderLines = parts + + # Submit the GSX repair request + result = gsx_repair.create() + + po = self.create_purchase_order() + + for p in self.servicepart_set.all(): + p.purchase_order = po + p.created_by = self.created_by + p.save() + + poi = PurchaseOrderItem.from_soi(po, p.order_item, self.created_by) + poi.save() + + confirmation = result.confirmationNumber + self.confirmation = confirmation + self.submitted_at = timezone.now() + + po.confirmation = confirmation + po.submit(self.created_by) + + self.save() + + msg = _(u"GSX repair %s created") % confirmation + self.order.notify("gsx_repair_created", msg, self.created_by) + + if repair_data.get("markCompleteFlag") is True: + self.close(self.created_by) + + def get_gsx_repair(self): + return gsxws.CarryInRepair(self.confirmation) + + def get_unit_received(self): + """ + Returns (as a tuple) the GSX-compatible date and time of + when this unit was received + """ + import locale + langs = gsxws.get_format('en_XXX') + ts = self.unit_received_at + loc = locale.getlocale() + # reset locale to get correct AM/PM value + locale.setlocale(locale.LC_TIME, None) + result = ts.strftime(langs['df']), ts.strftime(langs['tf']) + locale.setlocale(locale.LC_TIME, loc) + return result + + def get_received_date(self): + return self.get_unit_received()[0] + + def to_gsx(self): + """ + Returns this Repair as a GSX-compatible dict + """ + data = {'serialNumber': self.device.sn} + data['notes'] = self.notes + data['symptom'] = self.symptom + data['poNumber'] = self.reference + data['diagnosis'] = self.diagnosis + data['shipTo'] = self.gsx_account.ship_to + # checkIfOutOfWarrantyCoverage + if self.tech_id: + data['diagnosedByTechId'] = self.tech_id + + ts = self.get_unit_received() + data['unitReceivedDate'] = ts[0] + data['unitReceivedTime'] = ts[1] + + if self.attachment: + data['fileData'] = self.attachment + data['fileName'] = os.path.basename(self.attachment.name) + + if self.mark_complete: + data['markCompleteFlag'] = self.mark_complete + data['replacementSerialNumber'] = self.replacement_sn + + data['requestReviewByApple'] = self.request_review + + if self.consumer_law is not None: + data['consumerLawEligible'] = self.consumer_law + + return data + + def has_serialized_parts(self): + """ + Checks if this Repair has any serialized parts + """ + count = self.parts.filter(servicepart__order_item__product__is_serialized=True).count() + return count > 0 + + def check_components(self): + """ + Runs GSX component check for this repair's parts + """ + l = gsxws.Lookup(serialNumber=self.device.sn) + l.repairStrategy = self.repair_type + l.shipTo = self.gsx_account.ship_to + parts = [] + + for i in self.servicepart_set.all(): + part = gsxws.ServicePart(i.part_number) + part.symptomCode = i.comptia_code + parts.append(part) + + try: + r = l.component_check(parts) + except gsxws.GsxError, e: + if e.code == "COMP.LKP.004": + return # Symptom Code not required for Replacement and Other category parts. + raise e + + if r.componentDetails is None: + return + + if len(self.component_data) < 1: + d = {} + for i in r.componentDetails: + f = i.componentCode + d[f] = i.componentDescription + + self.component_data = json.dumps(d) + + return self.component_data + + def connect_gsx(self, user=None): + """ + Initialize the GSX session with the right credentials. + User can also be different from the one who initially created the repair. + """ + account = user or self.created_by + self.gsx_account.connect(account) + + def set_status(self, new_status, user): + """ + Sets the current status of this repair to new_status + and notifies the corresponding Service Order + """ + if not new_status == self.status: + self.status = new_status + self.save() + self.order.notify("repair_status_changed", self.status, user) + + def update_status(self, user): + repair = self.get_gsx_repair() + status = repair.status().repairStatus + self.set_status(status, user) + + return self.status + + def get_details(self): + repair = self.get_gsx_repair() + details = repair.details() + + if isinstance(details.partsInfo, dict): + details.partsInfo = [details.partsInfo] + + self.update_details(details) + return details + + def get_return_label(self, part): + self.get_details() + part = self.servicepart_set.get(pk=part) + return part.get_return_label() + + def update_details(self, details): + """ + Updates what local info we have about this particular GSX repair + """ + part_list = list(self.servicepart_set.all().order_by('id')) + + for i, p in enumerate(details.partsInfo): + try: + part = part_list[i] + part.set_part_details(p) + part.save() + except IndexError: # part added in GSX web ui... + self.add_gsx_part(p) + except AttributeError: # some missing attribute in set_part_details() + pass + + def get_replacement_sn(self): + """ + Try to guess replacement part's SN + """ + oi = self.order.serviceorderitem_set.filter( + product__is_serialized=True, + product__part_type="REPLACEMENT" + ) + + try: + return oi[0].sn + except IndexError: + pass + + def complete(self, user): + self.completed_at = timezone.now() + self.completed_by = user + self.save() + + queue = self.order.queue + if queue.status_repair_completed: + status = queue.status_repair_completed + self.order.set_status(status, user) + + def close(self, user): + """ + Marks this GSX repair as complete + """ + self.connect_gsx(user) + repair = self.get_gsx_repair() + + try: + # Update part serial numbers + [part.update_sn() for part in self.servicepart_set.all()] + repair.mark_complete() + except gsxws.GsxError as e: + """ + Valid GSX errors are: + 'ACT.BIN.01': Repair # provided is not valid. Please enter a valid repair #. + 'RPR.LKP.01': No Repair found matching search criteria. + 'RPR.LKP.010': No Repair found matching the search criteria. + 'RPR.COM.030': Cannot mark repair as complete for Unit $1. Repair is not open. + 'RPR.COM.036': Repair for Unit $1 is already marked as complete. + 'RPR.COM.019': This repair cannot be updated. + 'RPR.LKP.16': This Repair Cannot be Updated.Repair is not Open. + 'RPR.COM.136': Repair $1 cannot be marked complete as the Warranty + Claims Certification Form status is either Declined or Hold. + 'ENT.UPL.022': 'Confirmation # $1 does not exist.' + """ + errorlist = ( + 'ACT.BIN.01', + 'RPR.LKP.01', + 'RPR.LKP.010', + 'RPR.COM.030', + 'RPR.COM.036', + 'RPR.COM.019', + 'RPR.LKP.16', + 'RPR.COM.136', + 'ENT.UPL.022', + ) + + if e.code not in errorlist: + raise e + + status = repair.status() + self.set_status(status.repairStatus, user) + + self.complete(user) + + def duplicate(self, user): + """ + Makes a copy of this GSX Repair + """ + new_rep = Repair(order=self.order, created_by=user, device=self.device) + new_rep.repair_type = self.repair_type + new_rep.tech_id = self.tech_id + new_rep.symptom = self.symptom + new_rep.diagnosis = self.diagnosis + new_rep.notes = self.notes + new_rep.reference = self.reference + new_rep.request_review = self.request_review + new_rep.mark_complete = self.mark_complete + new_rep.unit_received_at = self.unit_received_at + new_rep.attachment = self.attachment + new_rep.gsx_account = self.gsx_account + + new_rep.save() + new_rep.set_parts(self.order.get_parts()) + + return new_rep + + def get_absolute_url(self): + if self.submitted_at is None: + return reverse('repairs-edit_repair', args=[self.order.pk, self.pk]) + return reverse('repairs-view_repair', args=[self.order.pk, self.pk]) + + def __unicode__(self): + if self.pk is not None: + return _("Repair %d") % self.pk + + class Meta: + app_label = "servo" + get_latest_by = "created_at" -- cgit v1.2.3