# -*- coding: utf-8 -*- from datetime import timedelta from django.db import models, IntegrityError from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.contrib.postgres.fields import ArrayField from django.contrib.contenttypes.fields import GenericRelation from django.urls import reverse from django.dispatch import receiver from django.db.models.signals import pre_save, post_save, post_delete from servo import defaults from servo.lib.shorturl import encode_url from servo.models.common import Tag, Location, Event, Configuration, GsxAccount from servo.models.product import * from servo.models.customer import Customer from servo.models.device import Device from servo.models.queue import Queue, Status, QueueStatus class Order(models.Model): """The Service Order.""" code = models.CharField(max_length=8, unique=True, null=True) url_code = models.CharField(max_length=8, unique=True, null=True) # Device description or something else description = models.CharField(max_length=128, default="") status_icon = models.CharField(max_length=16, default="undefined") priority = models.IntegerField( default=Queue.PRIO_NORMAL, choices=Queue.PRIORITIES, verbose_name=_("priority") ) created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, related_name="created_orders", on_delete=models.SET_NULL ) started_at = models.DateTimeField(null=True) started_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, related_name="started_orders", on_delete=models.SET_NULL ) unit_received_at = models.DateTimeField(null=True) customer_contacted_at = models.DateTimeField(null=True) closed_at = models.DateTimeField(null=True) closed_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, related_name='closed_orders', on_delete=models.SET_NULL ) followed_by = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name="followed_orders" ) tags = models.ManyToManyField(Tag, verbose_name="tags") events = GenericRelation(Event) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT ) checkin_location = models.ForeignKey( Location, null=True, related_name='+', on_delete=models.SET_NULL ) location = models.ForeignKey(Location, on_delete=models.PROTECT) checkout_location = models.ForeignKey( Location, null=True, related_name='+', on_delete=models.SET_NULL ) place = models.CharField(default='', max_length=128) customer = models.ForeignKey( Customer, null=True, related_name='orders', on_delete=models.SET_NULL ) customer_name = models.CharField(max_length=128, default='') devices = models.ManyToManyField(Device, through="OrderDevice") products = models.ManyToManyField(Product, through="ServiceOrderItem") queue = models.ForeignKey( Queue, null=True, verbose_name=_("queue"), on_delete=models.SET_NULL ) status = models.ForeignKey( QueueStatus, null=True, verbose_name=_("status"), on_delete=models.SET_NULL ) statuses = models.ManyToManyField( Status, through="OrderStatus", related_name="orders" ) STATE_QUEUED = 0 # order hasn't been started STATE_OPEN = 1 # order is being worked on STATE_CLOSED = 2 # order is closed STATE_WAITING = 3 # order is waiting (do not track duration) STATES = ( (STATE_QUEUED, _("Unassigned")), (STATE_OPEN, _("Open")), (STATE_CLOSED, _("Closed")), (STATE_WAITING, _("Waiting")) ) state = models.IntegerField(default=STATE_QUEUED, choices=STATES) status_name = models.CharField(max_length=128, default="") status_started_at = models.DateTimeField(null=True) status_limit_green = models.DateTimeField(null=True) # turn yellow after this status_limit_yellow = models.DateTimeField(null=True) # turn red after this api_fields = ('status_name', 'status_description',) def get_issues(self): return self.note_set.filter(type=1) def apply_rules(self): pass def get_accessories(self): return Accessory.objects.filter(order=self) def set_accessories(self, accs, device): for a in accs: a = Accessory(name=a, order=self, device=device) a.save() def add_tag(self, tag, user): from servo.tasks import apply_rules if not isinstance(tag, Tag): tag = Tag.objects.get(pk=tag) self.tags.add(tag) event = Event(content_object=self) event.description = str(tag.pk) event.action = "set_tag" event.triggered_by = user apply_rules(event) def set_tags(self, tags, user): return [self.add_tag(t, user) for t in tags] def check_in(self, user): """Check this Order in through the check-in process.""" queue_id = Configuration.conf('checkin_queue') self.set_queue(queue_id, user) def can_order_products(self): return self.products.count() > 0 and self.is_editable def duplicate(self, user): new_order = Order(customer=self.customer, created_by=user) new_order.save() new_order.set_queue(self.queue_id, user) for d in self.devices.all(): new_order.add_device(d, user) return new_order def get_print_template(self, kind="confirmation"): """Return the name of the correct print template for this order. Based on the order's queue """ template = "orders/print_%s.html" % kind if self.queue: queue = self.queue if kind == "confirmation" and queue.order_template: template = queue.order_template.name if kind == "quote" and queue.quote_template: template = queue.quote_template.name if kind == "receipt" and queue.receipt_template: template = queue.receipt_template.name if kind == "dispatch" and queue.dispatch_template: template = queue.dispatch_template.name return template def get_repairs(self): # Returns the active GSX repairs for this SO return self.repair_set.exclude(submitted_at=None) def get_repair(self): # Returns the latest GSX repair for this SO try: return self.get_repairs().latest() except Exception: return def get_similar(self, status, state): # Returns a queryset of "similar" cases if self.user is None: return Order.objects.filter(status=status) return Order.objects.filter(user=self.user) def get_footer(self): footer = self.code repair = self.get_repair() if repair: footer += ' (%s)' % repair.confirmation return footer def add_device(self, device, user): try: OrderDevice.objects.create(order=self, device=device) event = _(u'%s added') % device.description self.notify('device_added', event, user) return event except IntegrityError: raise ValueError(_("This device has already been added to this order")) def add_device_sn(self, sn, user): """ Adds device to order using serial number """ sn = sn.upper() device = Device.objects.filter(sn=sn).first() if device is None: device = Device.from_gsx(sn) device.save() self.add_device(device, user) return device def remove_device(self, device, user): OrderDevice.objects.filter(order=self, device=device).delete() msg = _(u'%s removed') % device.description self.notify('device_removed', msg, user) return msg def get_available_users(self, user): """ Returns a list of users available to work on this order """ if self.queue: return self.queue.user_set.filter(is_active=True) return user.location.user_set.filter(is_active=True) def get_print_dict(self, kind='confirmation'): """ Return context dict for printing this order """ r = {'order': self} r['conf'] = Configuration.conf() r['title'] = _(u"Service Order #%s") % self.code r['notes'] = self.note_set.filter(is_reported=True) # TODO: replace with constants r['issues'] = r['notes'].filter(type=1) r['diagnoses'] = r['notes'].filter(type=3) r['tech_notes'] = r['notes'].filter(type=0) r['verified_issues'] = r['notes'].filter(type=4) r['customer_notes'] = r['notes'].filter(type=5) if kind == 'receipt': try: # Include the latest invoice data for receipts r['invoice'] = self.invoice_set.latest() except Exception as e: pass return r def get_title(self): """ Returns a human-readable title for this order, based on various criteria """ from django.utils.timesince import timesince now = timezone.now() moment_seconds = 120 if self.closed_at: if (now - self.closed_at).seconds < moment_seconds: return _("Closed a moment ago") return _(u"Closed for %(time)s") % {'time': timesince(self.closed_at)} if self.status and self.status_started_at is not None: if (now - self.status_started_at).seconds < moment_seconds: return _(u"%s a moment ago") % self.status.status.title delta = timesince(self.status_started_at) d = {'status': self.status.status.title, 'time': delta} return _("%(status)s for %(time)s" % d) if self.user is None: if (now - self.created_at).seconds < moment_seconds: return _("Created a moment ago") return _("Unassigned for %(delta)s") % {'delta': timesince(self.created_at)} if self.in_progress(): if (now - self.started_at).seconds < moment_seconds: return _("Started a moment ago") return _("Open for %(delta)s") % {'delta': timesince(self.started_at)} def in_progress(self): return self.started_at and self.user is not None def get_place(self): return self.place or _("Select place") def get_status(self): return self.status or _("Select status") def get_user_name(self): if self.user is not None: return self.user.get_full_name() def get_user(self): return self.user or _("Select user") def get_queue(self): return self.queue or _("Select queue") def get_queue_url(self): if self.queue is not None: return self.queue.get_absolute_url() return reverse("orders-index") def get_queue_title(self): if self.queue is not None: return self.queue.title else: return _("Orders") def is_item_complete(self, item): try: return self.checklistitemvalue_set.get(item=item) except Exception: return False def close(self, user): """Close this service order.""" if Configuration.autocomplete_repairs(): for r in self.repair_set.active(): try: r.set_status_code('RFPU') r.close(user) except Exception as e: # notify the creator of the GSX repair instead of just erroring out e = self.notify("gsx_error", e, user) e.notify_users.add(r.created_by) if self.queue and self.queue.status_closed: self.set_status(self.queue.status_closed, user) self.notify("close_order", _(u"Order %s closed") % self.code, user) self.closed_by = user self.closed_at = timezone.now() self.state = self.STATE_CLOSED self.save() def reopen(self, user): """ Re-opens this service order """ self.state = Order.STATE_OPEN self.closed_at = None self.save() msg = _("Order %s reopened") % self.code self.notify("reopen", msg, user) return msg def notes(self): return self.note_set.all() def reported_notes(self): return self.note_set.filter(is_reported=True) @property def can_create_carryin(self): return self.customer and self.queue and self.is_editable def get_status_name(self): try: return self.status.status.title except Exception: pass def get_status_description(self): if self.status is None: return _('Order is waiting to be processed') else: return self.status.status.description def get_status_id(self): """ Returns "real" status ID of this order (regardless of queue) """ if self.status: return self.status.status.id def get_next(self): try: result = Order.objects.filter(pk__gt=self.pk, queue=self.queue) return result[0].pk except Exception: pass def get_prev(self): try: result = Order.objects.filter(pk__lt=self.pk, queue=self.queue) return result[0].pk except Exception: pass def get_color(self): color = "undefined" if self.status: now = timezone.now() if now > self.status_limit_yellow: color = "danger" if now < self.status_limit_yellow: color = "warning" if now < self.status_limit_green: color = "success" return color def get_status_img(self): color = self.get_color() return "images/status_%s_16.png" % color def set_property(self, key, value): pass def set_location(self, new_location, user): """ Moves order to new location """ # move the products too for soi in self.serviceorderitem_set.all(): product = soi.product try: source = Inventory.objects.get(location=self.location, product=product) source.move(new_location, soi.amount) except Inventory.DoesNotExist: pass # @TODO: Is this OK? self.location = new_location msg = _(u"Order %s moved to %s") % (self.code, new_location.title) self.notify("set_location", msg, user) self.save() return msg def set_checkin_location(self, new_location, user): pass def notify(self, action, message, user): """ Notifies this order of an event This is also the hub for automation handling """ if self.is_closed: return e = Event(content_object=self, action=action) e.description = message e.triggered_by = user e.save() for f in self.followed_by.exclude(pk=user.pk).exclude(should_notify=False): e.notify_users.add(f) if action == "product_arrived": if self.queue and self.queue.status_products_received: new_status = self.queue.status_products_received self.set_status(new_status, user) return e def set_status(self, new_status, user): """ Sets status of this order to new_status Status can only be set if order belongs to a queue! """ if self.is_closed: return # fail silently if self.queue is None: raise ValueError(_('Order must belong to a queue to set status')) if isinstance(new_status, QueueStatus): status = new_status else: if int(new_status) == 0: return self.unset_status(user) status = QueueStatus.objects.get(pk=new_status) self.status = status self.status_name = status.status.title self.status_started_at = timezone.now() self.status_limit_green = status.get_green_limit() self.status_limit_yellow = status.get_yellow_limit() self.save() # Set up the OrderStatus OrderStatus.create(self, status, user) # trigger the notification self.notify("set_status", self.status_name, user) def unset_status(self, user): if self.is_closed: return # fail silently self.status = None self.status_started_at = None self.status_limit_green = None self.status_limit_yellow = None self.save() self.notify("set_status", _('Status unassigned'), user) def set_queue(self, queue_id, user): if self.is_closed: return if queue_id in (None, ''): queue_id = 0 if isinstance(queue_id, Queue): queue = queue_id else: if int(queue_id) == 0: return self.unset_queue(user) queue = Queue.objects.get(pk=queue_id) self.queue = queue self.priority = queue.priority self.notify('set_queue', queue.title, user) if queue.gsx_soldto: gsx_account = GsxAccount.get_account(self.location, queue) for i in self.repair_set.filter(completed_at=None): i.gsx_account = gsx_account i.save() if queue.status_created: self.set_status(queue.status_created, user) else: self.save() def unset_queue(self, user): self.queue = None self.notify('set_queue', _('Removed from queue'), user) self.save() def add_follower(self, follower): if follower in self.followed_by.all(): return self.followed_by.add(follower) def remove_follower(self, follower): if self.state == self.STATE_CLOSED: raise ValueError(_('Closed orders cannot be modified')) self.followed_by.remove(follower) def toggle_follower(self, follower): if follower in order.followed_by.all(): self.remove_follower(user) else: self.add_follower(follower) def set_user(self, new_user, current_user): """ Sets the assignee of this order to new_user """ if self.state == self.STATE_CLOSED: raise ValueError(_('Closed orders cannot be modified')) state = self.STATE_OPEN if new_user is None: state = self.STATE_QUEUED event = _("Order unassigned") self.remove_follower(self.user) else: data = {'order': self.code, 'user': new_user.get_full_name()} event = _(u"Order %(order)s assigned to %(user)s") % data # The assignee should also be a follower self.add_follower(new_user) self.user = new_user self.state = state self.notify("set_user", event, current_user) if self.user is not None: self.location = new_user.location if self.started_by is None: self.started_by = new_user self.started_at = timezone.now() queue = self.queue if queue and queue.status_assigned: self.set_status(queue.status_assigned, current_user) self.save() def customer_id(self): return self.customer.id def customer_list(self): """ Returns this order's customer wrapped in a list for easier tree recursion """ return [self.customer] def customer_tree(self): if self.customer is None: return '0' else: return self.customer.tree_id def has_devices(self): return self.devices.all().count() > 0 def device_name(self): if self.devices.count(): return self.devices.all()[0].description def set_customer(self, new_customer): self.customer = new_customer self.save() def get_customer_name(self): try: return self.customer.fullname except AttributeError: pass def device_slug(self): try: return self.devices.all()[0].slug except Exception: pass def net_total(self): total = 0 for p in self.serviceorderitem_set.filter(should_report=True): total += p.price_notax() * p.amount return total def gross_total(self): total = 0 for p in self.serviceorderitem_set.filter(should_report=True): total += p.price * p.amount return total def total_tax(self): return self.gross_total() - self.net_total() def add_product(self, product, amount, user): """ Adds this product to the Service Order with stock price """ oi = ServiceOrderItem(order=self, created_by=user) oi.product = product oi.code = product.code oi.title = product.title oi.description = product.description oi.amount = amount oi.price_category = 'stock' oi.price = product.price_sales_stock oi.save() self.notify("product_added", _('Product %s added') % oi.title, user) return oi def remove_product(self, oi, user): oi.delete() msg = _('Product %s removed from order') % oi.title self.notify("product_removed", msg, user) return msg @property def can_dispatch(self): undispatched = self.products.filter(dispatched=False) return undispatched.count() > 0 and self.is_editable @property def can_close(self): return self.is_editable and not self.can_dispatch def dispatch(self, invoice, products): """ Dispatch these products from the inventory with this invoice """ invoice.dispatch(products) if self.queue and self.queue.status_dispatched: self.set_status(self.queue.status_dispatched, invoice.created_by) def total_margin(self): """ Calculates the total margin for this Service Order """ total_purchase_price = 0 for p in self.serviceorderitem_set.filter(should_report=True): total_purchase_price += p.get_product_price('purchase') * p.amount return (self.net_total() - Decimal(total_purchase_price)) @property def products(self): return self.serviceorderitem_set.filter(should_report=True) @property def serialized_products(self): return self.products.filter(product__is_serialized=True) def get_parts(self): """ Returns the GSX parts that can be ordered for this SRO """ return [x for x in self.products.all() if x.product.is_apple_part] @property def has_parts(self): return len(self.get_parts()) > 0 def get_devices(self): return self.devices.filter(orderdevice__should_report=True) def get_first_device(self): try: return self.get_devices().first() except Exception: pass @property def is_editable(self): return self.closed_at is None @property def is_closed(self): return self.closed_at is not None @property def has_products(self): return self.products.count() > 0 def has_accessories(self): return self.accessory_set.all().count() > 0 def get_accessories(self): return self.accessory_set.values_list('name', flat=True) class Meta: app_label = 'servo' ordering = ('-priority', 'id',) permissions = ( ("change_user", _("Can set assignee")), ("change_status", _("Can change status")), ("follow_order", _("Can follow order")), ("copy_order", _("Can copy order")), ("batch_process", _("Can batch process")), ) def save(self, *args, **kwargs): location = self.created_by.location if self.location_id is None: self.location = location if self.checkin_location is None: self.checkin_location = location if self.checkout_location is None: self.checkout_location = location if self.customer and self.customer_name == '': self.customer_name = self.customer.fullname super(Order, self).save(*args, **kwargs) if self.code is None: self.url_code = encode_url(self.id).upper() self.code = settings.INSTALL_ID + str(self.id).rjust(6, '0') event = _('Order %s created') % self.code self.notify('created', event, self.created_by) self.save() def get_absolute_url(self): return reverse("orders-edit", args=[self.pk]) def __str__(self): return self.code class AbstractOrderItem(models.Model): """ The base class for order lines (purchase, sales) """ product = models.ForeignKey(Product, on_delete=models.PROTECT) code = models.CharField(blank=True, default='', max_length=128) title = models.CharField(max_length=128, verbose_name=_("title")) description = models.TextField( blank=True, default='', verbose_name=_("description") ) created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, editable=False, on_delete=models.SET_NULL ) amount = models.IntegerField(default=1, verbose_name=_("amount")) sn = models.CharField( blank=True, default="", max_length=32, verbose_name=_("KGB Serial Number") ) def price_notax(self): """ Returns the price of this OI w/o VAT """ from decimal import ROUND_05UP vat_pct = self.product.pct_vat return (self.price/Decimal((100+vat_pct)/100)).quantize(Decimal('1.00')) def total_gross(self): return self.price * self.amount def total_net(self): return self.price_notax() * self.amount def total_tax(self): """ Returns the amount of VAT paid for this POI """ return (self.price - self.price_notax()) * self.amount class Meta: abstract = True class ServiceOrderItem(AbstractOrderItem): """ A product that has been added to a Service Order """ order = models.ForeignKey(Order, on_delete=models.CASCADE) dispatched = models.BooleanField( default=False, verbose_name=_("dispatched") ) should_report = models.BooleanField( default=True, verbose_name=_("report") ) price = models.DecimalField( max_digits=8, decimal_places=2, verbose_name=_('sales price') ) replaced_at = models.DateTimeField(null=True) replaced_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, editable=False, on_delete=models.SET_NULL, related_name='replaced_parts' ) kbb_sn = models.CharField( blank=True, default="", max_length=32, verbose_name=_("KBB Serial Number") ) imei = models.CharField( blank=True, default="", max_length=35, verbose_name=_("IMEI") ) PRICE_CATEGORIES = ( ('warranty', _("Warranty")), ('exchange', _("Exchange Price")), ('stock', _("Stock Price")), ) price_category = models.CharField( max_length=32, choices=PRICE_CATEGORIES, default=PRICE_CATEGORIES[0], verbose_name=_("Price category") ) comptia_code = models.CharField( blank=True, default="", max_length=4, verbose_name=_("Symptom code") ) comptia_modifier = models.CharField( blank=True, default="", max_length=1, verbose_name=_("Symptom modifier") ) def can_create_device(self): pt = self.product.part_type return pt == 'REPLACEMENT' and self.sn def comptia_choices(self): if self.product is not None: from servo.models.parts import symptom_codes return symptom_codes(self.product.component_code) def get_comptia_code_display(self): for i in self.comptia_choices(): if i[0] == self.comptia_code: return i[1] def get_comptia_modifier_display(self): if self.comptia_modifier: from servo.models.parts import symptom_modifiers for m in symptom_modifiers(): if m[0] == self.comptia_modifier: return m[1] def get_part(self): return self.servicepart_set.latest() def get_poitem(self): return self.purchaseorderitem_set.latest() def get_repair(self): return self.order.repair_set.latest() def reserve_product(self): """ Reserve this SOI for the inventory at this location """ location = self.order.location inventory, created = Inventory.objects.get_or_create(location=location, product=self.product) inventory.amount_reserved += self.amount inventory.save() def get_purchase_price(self): """ Returns the purchase price of this SOIs Product """ return self.product.get_price(self.price_category, "purchase") def get_product_price(self, kind): return self.product.get_price(category=self.price_category, kind=kind) def save(self, *args, **kwargs): self.sn = self.sn.upper() self.kbb_sn = self.kbb_sn.upper() return super(ServiceOrderItem, self).save(*args, **kwargs) @property def is_abused(self): return self.price_category == 'stock' @property def is_warranty(self): return self.price_category == 'warranty' def __str__(self): return self.code class Meta: app_label = "servo" ordering = ('id',) get_latest_by = "id" class OrderStatus(models.Model): """ The M/M statuses of an order """ order = models.ForeignKey(Order, on_delete=models.CASCADE) status = models.ForeignKey(Status, on_delete=models.CASCADE) started_at = models.DateTimeField(auto_now_add=True) finished_at = models.DateTimeField(null=True) started_by = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='+', on_delete=models.CASCADE, ) finished_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, related_name='+', on_delete=models.CASCADE, ) green_limit = models.DateTimeField(null=True) yellow_limit = models.DateTimeField(null=True) BADGES = ( ('undefined', 'undefined'), ('success', 'success'), ('warning', 'warning'), ('danger', 'danger'), ) badge = models.CharField(choices=BADGES, default=BADGES[0][0], max_length=16) duration = models.IntegerField(default=0) @classmethod def create(cls, order, queue_status, user): """ Set status or order to queue_status.status """ new_status = queue_status.status os = cls(order=order, status=new_status) os.started_by = user #os.started_at = timezone.now() os.green_limit = queue_status.get_green_limit() os.yellow_limit = queue_status.get_yellow_limit() os.save() prev = os.get_previous() if prev is None: return # set color of previous OS if prev.finished_by is None: prev.finished_by = user prev.finished_at = timezone.now() prev.duration = (prev.finished_at - prev.started_at).total_seconds() prev.badge = prev.get_badge() prev.save() def get_badge(self): now = timezone.now() badge = "undefined" if self.yellow_limit and now > self.yellow_limit: badge = "danger" if self.yellow_limit and now < self.yellow_limit: badge = "warning" if self.green_limit and now < self.green_limit: badge = "success" return badge def get_next(self): statuses = self.order.orderstatus_set return statuses.filter(started_at__gt=self.started_at).order_by('id').first() def get_previous(self): statuses = self.order.orderstatus_set return statuses.filter(started_at__lt=self.started_at).order_by('id').last() def __str__(self): return self.status.title class Meta: app_label = "servo" ordering = ('-started_at',) get_latest_by = "started_at" class OrderDevice(models.Model): """ A device attached to a service order """ order = models.ForeignKey(Order, on_delete=models.CASCADE) device = models.ForeignKey(Device, on_delete=models.PROTECT) should_report = models.BooleanField(default=True) repeat_service = models.BooleanField(default=False) repair_strategies = ArrayField(models.CharField(max_length=100), help_text='Available repair strategies from GSX', null=True) def is_repeat_service(self): """ Returns true if this is a repeat (< 30 days from last) service for this device """ created_at = self.order.created_at tlimit = timezone.now() - timedelta(days=30) orders = Order.objects.filter(orderdevice__device=self.device, created_at__lt=created_at, created_at__gte=tlimit) return orders.exclude(pk=self.order.pk).count() > 0 class Meta: app_label = "servo" unique_together = ('order', 'device', ) # Can't add the same device more than once class Accessory(models.Model): """ An accessory that came with the device in this Service Order """ name = models.TextField() qty = models.IntegerField(default=1) device = models.ForeignKey(Device, on_delete=models.CASCADE) order = models.ForeignKey(Order, on_delete=models.CASCADE) def __str__(self): return self.name class Meta: app_label = "servo" @receiver(post_save, sender=OrderDevice) def trigger_orderdevice_saved(sender, instance, created, **kwargs): order = instance.order device = instance.device order.description = device.description if order.queue is None: pass # @TODO try to autoasign case to right queue... order.save() @receiver(post_delete, sender=OrderDevice) def trigger_device_removed(sender, instance, **kwargs): try: order = instance.order except Order.DoesNotExist: return # Means the whole order was deleted, not just the device devices = order.devices.all() if devices.count() > 0: order.description = devices[0].description else: order.description = '' order.save()