text: # -*- coding: utf-8 -*-
  from __future__ import unicode_literals
  
  from django.db.models import Sum
  from django.utils import timezone
  from django.utils.functional import cached_property
  from django.utils.translation import ugettext_lazy as _
  from django_fsm import transition
  from shop.models.delivery import DeliveryModel, DeliveryItemModel
  
  
  class SimpleShippingWorkflowMixin(object):
      """
      Workflow for simply marking the state of an Order while picking, packing and shipping items.
      It does not create a Delivery object.
  
      Add this class to ``settings.SHOP_ORDER_WORKFLOWS`` to mix it into the merchants Order model.
      It is mutual exclusive with :class:`shop.shipping.workflows.CommissionGoodsWorkflowMixin` or
      :class:`shop.shipping.workflows.PartialDeliveryWorkflowMixin`.
  
      It adds all the methods required for state transitions, while picking and packing
      the ordered goods for shipping.
      """
      TRANSITION_TARGETS = {
          'pick_goods': _("Picking goods"),
          'pack_goods': _("Packing goods"),
          'ship_goods': _("Prepare for shipping"),
          'ready_for_delivery': _("Ready for delivery"),
      }
  
      @property
      def associate_with_delivery(self):
          """
          :returns: ``True`` if this Order requires a delivery object.
          """
          return False
  
      @property
      def allow_partial_delivery(self):
          """
          :returns: ``True`` if partial item delivery is allowed.
          """
          return False
  
      @transition(field='status', source='payment_confirmed', target='pick_goods',
                  custom=dict(admin=True, button_name=_("Pick the goods")))
      def pick_goods(self, by=None):
          """Change status to 'pick_goods'."""
  
      @transition(field='status', source='pick_goods', target='pack_goods',
                  custom=dict(admin=True, button_name=_("Pack the goods")))
      def pack_goods(self, by=None):
          """Change status to 'pack_goods'."""
  
      @transition(field='status', source='pack_goods', target='ship_goods',
                  custom=dict(admin=True, button_name=_("Prepare for shipping")))
      def ship_goods(self, by=None):
          """
          Ship the goods. This method implicitly invokes
          :method:`shop.shipping.modifiers.ShippingModifier.ship_the_goods(delivery)`
          """
  
      @transition(field='status', source='ship_goods', target='ready_for_delivery',
                  custom=dict(auto=True))
      def prepare_for_delivery(self, by=None):
          """
          Put the parcel into the outgoing delivery.
          This method is invoked automatically by `ship_goods`.
          """
  
      def update_or_create_delivery(self, orderitem_data):
          """
          Hook to create a delivery object with items.
          """
  
  
  class CommissionGoodsWorkflowMixin(SimpleShippingWorkflowMixin):
      """
      Workflow to commission all ordered items in one common Delivery.
  
      Add this class to ``settings.SHOP_ORDER_WORKFLOWS`` to mix it into the merchants Order model.
      It is mutual exclusive with :class:`shop.shipping.workflows.SimpleShippingWorkflowMixin` or
      :class:`shop.shipping.workflows.PartialDeliveryWorkflowMixin`.
  
      It adds all the methods required for state transitions, while picking and packing
      the ordered goods for shipping.
      """
      @property
      def associate_with_delivery(self):
          return True
  
      @transition(field='status', source='ship_goods', target='ready_for_delivery',
                  custom=dict(auto=True))
      def prepare_for_delivery(self, by=None):
          """Put the parcel into the outgoing delivery."""
  
      def update_or_create_delivery(self, orderitem_data):
          """
          Update or create a Delivery object for all items of this Order object.
          """
          delivery, _ = DeliveryModel.objects.get_or_create(
              order=self,
              shipping_id__isnull=True,
              shipped_at__isnull=True,
              shipping_method=self.extra.get('shipping_modifier'),
              defaults={'fulfilled_at': timezone.now()}
          )
          for item in self.items.all():
              DeliveryItemModel.objects.create(
                  delivery=delivery,
                  item=item,
                  quantity=item.quantity,
              )
  
  
  class PartialDeliveryWorkflowMixin(CommissionGoodsWorkflowMixin):
      """
      Workflow to optionally commission ordered items partially.
  
      Add this class to ``settings.SHOP_ORDER_WORKFLOWS`` to mix it into the merchants Order model.
      It is mutual exclusive with :class:`shop.shipping.workflows.SimpleShippingWorkflowMixin` or
      :class:`shop.shipping.workflows.CommissionGoodsWorkflowMixin`.
  
      This mixin supports partial delivery, hence check that a materialized representation of the
      models :class:`shop.models.delivery.DeliveryModel` and :class:`shop.models.delivery.DeliveryItemModel`
      exists and is instantiated.
  
      Importing the classes :class:`shop.models.defaults.delivery.DeliveryModel` and
      :class:`shop.models.defaults.delivery_item.DeliveryItemModel` into the merchants
      ``models.py``, usually is enough. This adds all the methods required for state transitions,
      while picking, packing and shipping the ordered goods for delivery.
      """
      @property
      def allow_partial_delivery(self):
          return True
  
      @cached_property
      def unfulfilled_items(self):
          unfulfilled_items = 0
          for order_item in self.items.all():
              if not order_item.canceled:
                  aggr = order_item.deliver_item.aggregate(delivered=Sum('quantity'))
                  unfulfilled_items += order_item.quantity - (aggr['delivered'] or 0)
          return unfulfilled_items
  
      def ready_for_picking(self):
          return self.is_fully_paid() and self.unfulfilled_items > 0
  
      def ready_for_shipping(self):
          return self.delivery_set.filter(shipped_at__isnull=True).exists()
  
      @transition(field='status', source='*', target='pick_goods', conditions=[ready_for_picking],
                  custom=dict(admin=True, button_name=_("Pick the goods")))
      def pick_goods(self, by=None):
          """Change status to 'pick_goods'."""
  
      @transition(field='status', source=['pick_goods'], target='pack_goods',
                  custom=dict(admin=True, button_name=_("Pack the goods")))
      def pack_goods(self, by=None):
          """Prepare shipping object and change status to 'pack_goods'."""
  
      @transition(field='status', source='*', target='ship_goods', conditions=[ready_for_shipping],
                  custom=dict(admin=True, button_name=_("Ship the goods")))
      def ship_goods(self, by=None):
          """Ship the goods."""
  
      @transition(field='status', source='ship_goods', target='ready_for_delivery',
                  custom=dict(auto=True))
      def prepare_for_delivery(self, by=None):
          """Put the parcel into the outgoing delivery."""
  
      def update_or_create_delivery(self, orderitem_data):
          """
          Update or create a Delivery object and associate with selected ordered items.
          """
          delivery, _ = DeliveryModel.objects.get_or_create(
              order=self,
              shipping_id__isnull=True,
              shipped_at__isnull=True,
              shipping_method=self.extra.get('shipping_modifier'),
              defaults={'fulfilled_at': timezone.now()}
          )
  
          # create a DeliveryItem object for each ordered item to be shipped with this delivery
          for data in orderitem_data:
              if data['deliver_quantity'] > 0 and not data['canceled']:
                  DeliveryItemModel.objects.create(
                      delivery=delivery,
                      item=data['id'],
                      quantity=data['deliver_quantity'],
                  )
          if not delivery.items.exists():
              # since no OrderItem was added to this delivery, discard it
              delivery.delete()
  