Repository: django-oscar/django-oscar

Issue:
Not able to apply custom benefit
I followed the following [http://django-oscar.readthedocs.io/en/releases-1.2/howto/how_to_create_a_custom_benefit.html](tutorial) I did the same and tried to apply the benefit using ```create_benefit``` present in the ```oscar.apps.offer.custom``` after appling I get a recursion error and the function also warns of recursion error if not applied ```__str__ ``` but even after that I am facing the same problem. Kindly help ASAP

I am using Oscar 1.3


Discussion:
I solved it by adding my own 
@property 
def name(): and 
@property
def description()
I believe we should leave a note in the docs about name property declaration when defining custom benefit.

Resolving patch:
diff --git a/src/oscar/apps/offer/abstract_models.py b/src/oscar/apps/offer/abstract_models.py
--- a/src/oscar/apps/offer/abstract_models.py
+++ b/src/oscar/apps/offer/abstract_models.py
@@ -26,6 +26,60 @@
 BrowsableRangeManager = get_class('offer.managers', 'BrowsableRangeManager')
 
 
+@python_2_unicode_compatible
+class BaseOfferMixin(models.Model):
+    class Meta:
+        abstract = True
+
+    def proxy(self):
+        """
+        Return the proxy model
+        """
+        klassmap = self.proxy_map
+        # Short-circuit logic if current class is already a proxy class.
+        if self.__class__ in klassmap.values():
+            return self
+
+        field_dict = dict(self.__dict__)
+        for field in list(field_dict.keys()):
+            if field.startswith('_'):
+                del field_dict[field]
+
+        if self.proxy_class:
+            klass = utils.load_proxy(self.proxy_class)
+            # Short-circuit again.
+            if self.__class__ == klass:
+                return self
+            return klass(**field_dict)
+        if self.type in klassmap:
+            return klassmap[self.type](**field_dict)
+        raise RuntimeError("Unrecognised %s type (%s)" % (self.__class__.__name__.lower(), self.type))
+
+    def __str__(self):
+        return self.name
+
+    @property
+    def name(self):
+        """
+        A plaintext description of the benefit/condition. Every proxy class
+        has to implement it.
+
+        This is used in the dropdowns within the offer dashboard.
+        """
+        proxy_instance = self.proxy()
+        if self.proxy_class and self == proxy_instance:
+            raise AssertionError('Name property is not defined on proxy class.')
+        return proxy_instance.name
+
+    @property
+    def description(self):
+        """
+        A description of the benefit/condition.
+        Defaults to the name. May contain HTML.
+        """
+        return self.name
+
+
 @python_2_unicode_compatible
 class AbstractConditionalOffer(models.Model):
     """
@@ -397,8 +451,7 @@ def products(self):
             structure=Product.CHILD)
 
 
-@python_2_unicode_compatible
-class AbstractBenefit(models.Model):
+class AbstractBenefit(BaseOfferMixin, models.Model):
     range = models.ForeignKey(
         'offer.Range',
         blank=True,
@@ -469,50 +522,6 @@ def proxy_map(self):
                 'offer.benefits', 'ShippingPercentageDiscountBenefit')
         }
 
-    def proxy(self):
-        klassmap = self.proxy_map
-
-        # Short-circuit logic if current class is already a proxy class.
-        if self.__class__ in klassmap.values():
-            return self
-
-        field_dict = dict(self.__dict__)
-        for field in list(field_dict.keys()):
-            if field.startswith('_'):
-                del field_dict[field]
-
-        if self.proxy_class:
-            klass = utils.load_proxy(self.proxy_class)
-            # Short-circuit again.
-            if self.__class__ == klass:
-                return self
-            return klass(**field_dict)
-
-        if self.type in klassmap:
-            return klassmap[self.type](**field_dict)
-        raise RuntimeError("Unrecognised benefit type (%s)" % self.type)
-
-    def __str__(self):
-        return self.name
-
-    @property
-    def name(self):
-        """
-        A plaintext description of the benefit. Every proxy class has to
-        implement it.
-
-        This is used in the dropdowns within the offer dashboard.
-        """
-        return self.proxy().name
-
-    @property
-    def description(self):
-        """
-        A description of the benefit.
-        Defaults to the name. May contain HTML.
-        """
-        return self.name
-
     def apply(self, basket, condition, offer):
         return results.ZERO_DISCOUNT
 
@@ -667,8 +676,7 @@ def shipping_discount(self, charge):
         return D('0.00')
 
 
-@python_2_unicode_compatible
-class AbstractCondition(models.Model):
+class AbstractCondition(BaseOfferMixin, models.Model):
     """
     A condition for an offer to be applied. You can either specify a custom
     proxy class, or need to specify a type, range and value.
@@ -712,51 +720,6 @@ def proxy_map(self):
                 'offer.conditions', 'CoverageCondition'),
         }
 
-    def proxy(self):
-        """
-        Return the proxy model
-        """
-        klassmap = self.proxy_map
-        # Short-circuit logic if current class is already a proxy class.
-        if self.__class__ in klassmap.values():
-            return self
-
-        field_dict = dict(self.__dict__)
-        for field in list(field_dict.keys()):
-            if field.startswith('_'):
-                del field_dict[field]
-
-        if self.proxy_class:
-            klass = utils.load_proxy(self.proxy_class)
-            # Short-circuit again.
-            if self.__class__ == klass:
-                return self
-            return klass(**field_dict)
-        if self.type in klassmap:
-            return klassmap[self.type](**field_dict)
-        raise RuntimeError("Unrecognised condition type (%s)" % self.type)
-
-    def __str__(self):
-        return self.name
-
-    @property
-    def name(self):
-        """
-        A plaintext description of the condition. Every proxy class has to
-        implement it.
-
-        This is used in the dropdowns within the offer dashboard.
-        """
-        return self.proxy().name
-
-    @property
-    def description(self):
-        """
-        A description of the condition.
-        Defaults to the name. May contain HTML.
-        """
-        return self.name
-
     def consume_items(self, offer, basket, affected_lines):
         pass
 


Test patch:
diff --git a/tests/_site/model_tests_app/models.py b/tests/_site/model_tests_app/models.py
--- a/tests/_site/model_tests_app/models.py
+++ b/tests/_site/model_tests_app/models.py
@@ -35,7 +35,13 @@ def can_apply_condition(self, product):
         return False
 
 
-class CustomBenefitModel(Benefit):
+class BaseOfferModel(models.Model):
+    class Meta:
+        abstract = True
+        app_label = 'tests'
+
+
+class CustomBenefitModel(BaseOfferModel, Benefit):
 
     name = 'Test benefit'
 
@@ -49,3 +55,17 @@ def __str__(self):
     @property
     def description(self):
         return self.name
+
+
+class CustomBenefitWithoutName(Benefit):
+    class Meta:
+        proxy = True
+        app_label = 'tests'
+
+    description = 'test'
+
+
+class CustomConditionWithoutName(Condition):
+    class Meta:
+        proxy = True
+        app_label = 'tests'
diff --git a/tests/integration/offer/test_custom.py b/tests/integration/offer/test_custom.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/offer/test_custom.py
@@ -0,0 +1,23 @@
+from django.test import TestCase
+
+from oscar.apps.offer import custom
+
+from tests._site.model_tests_app.models import CustomBenefitWithoutName, CustomConditionWithoutName
+
+
+class TestCustomBenefit(TestCase):
+    def setUp(self):
+        self.custom_benefit = custom.create_benefit(CustomBenefitWithoutName)
+
+    def test_benefit_raises_assert_on_missing_title(self):
+        with self.assertRaisesMessage(AssertionError, 'Name property is not defined on proxy class.'):
+            str(self.custom_benefit)
+
+
+class TestCustomCondition(TestCase):
+    def setUp(self):
+        self.custom_condition = custom.create_condition(CustomConditionWithoutName)
+
+    def test_benefit_raises_assert_on_missing_title(self):
+        with self.assertRaisesMessage(AssertionError, 'Name property is not defined on proxy class.'):
+            str(self.custom_condition)