Browse Source

Fix important fail in billing model. Previously invoicedetail relate Offer. But should relate OfferSubscription in order permit a member to have more than one sunscription to one offer. (eg 2 ADSL)
Add a mechanism to filter an inline admin foreignkey depending to a parent field. (eg OfferSubscription in InvoiceDetail depend Member selected in Invoice)
Use this mecanism in invoice admin change form

Fabs 11 years ago
parent
commit
f8d20e1a1e

+ 44 - 10
coin/billing/admin.py

@@ -1,13 +1,33 @@
 # -*- coding: utf-8 -*-
+from django import forms
 from django.contrib import admin
+from coin.filtering_queryset import LimitedAdminInlineMixin
 from coin.billing.models import Invoice, InvoiceDetail, Payment
+from coin.offers.models import OfferSubscription
 import autocomplete_light
 
 
-class InvoiceDetailInline(admin.StackedInline):
+class InvoiceDetailInline(LimitedAdminInlineMixin, admin.StackedInline):
     model = InvoiceDetail
     extra = 0
-    fields = (('label', 'amount', 'quantity', 'tax'), ('offer','period_from', 'period_to'))
+    fields = (('label', 'amount', 'quantity', 'tax'),
+        ('offersubscription','period_from', 'period_to'))
+
+    def get_filters(self, obj):
+        """
+        Le champ "Abonnement" est filtré afin de n'afficher que les abonnements
+        du membre choisi dans la facture. Si pas de membre alors renvoi
+        une liste vide
+        """
+        if obj and obj.member:
+            return (('offersubscription', {'member':obj.member}),)
+        else:
+            return (('offersubscription', None),)
+
+    def get_readonly_fields(self, request, obj=None):
+        if not obj or not obj.member:
+            return self.readonly_fields + ('offersubscription',)
+        return self.readonly_fields
 
 
 class PaymentInline(admin.StackedInline):
@@ -17,13 +37,27 @@ class PaymentInline(admin.StackedInline):
 
 
 class InvoiceAdmin(admin.ModelAdmin):
-	list_display = ('number', 'date', 'status', 'amount', 'member')
-	list_display_links = ('number', 'date')
-	inlines = [InvoiceDetailInline, PaymentInline]
-	fields = (('number', 'date', 'status'),
-			  ('date_due'),
-			  'member')
-	form = autocomplete_light.modelform_factory(Invoice)
-
+    list_display = ('number', 'date', 'status', 'amount', 'member')
+    list_display_links = ('number', 'date')
+    inlines = [InvoiceDetailInline, PaymentInline]
+    fields = (('number', 'date', 'status'),
+       ('date_due'),
+       'member')
+    form = autocomplete_light.modelform_factory(Invoice)
+
+    def get_formsets(self, request, obj=None):
+        """
+        Lorsque l'on est en création d'objet (obj=None) alors de renvoi pas les 
+        formsets des inlines.
+        Cela permet de ne pas afficher les champs détails de facture et paiement
+        tant que la facture n'a pas été enregistré.
+        Cette subtilité permet de s'assurer que le select "Abonnement" de 
+        InvoiceDetail est bien filtré avec le member de la facture
+        """
+        if obj:
+            for _ in super(InvoiceAdmin, self).get_formsets(request, obj):
+                yield _
+        else:
+            pass
 
 admin.site.register(Invoice, InvoiceAdmin)

+ 9 - 9
coin/billing/create_subscriptions_invoices.py

@@ -37,8 +37,7 @@ def create_member_invoice_for_a_period(member, date):
 	invoice = None
 
 	# Récupère les abonnements en cours du membre
-	offer_subscriptions = (
-		OfferSubscription.get_member_offer_subscriptions(member,date))
+	offer_subscriptions = member.get_active_subscriptions(date)
 
 	# Pour chaque abonnement
 	for offer_subscription in offer_subscriptions:
@@ -46,10 +45,10 @@ def create_member_invoice_for_a_period(member, date):
 		offer = offer_subscription.offer
 
 		# Recherche dans les factures déjà existantes de ce membre des items
-		# ayant cette offre pour lesquels la période de facturation englobe
+		# ayant cet abonnement pour lesquels la période de facturation englobe
 		# la date
 		invoicedetail_test_before = InvoiceDetail.objects.filter(
-			offer__exact=offer.pk,
+			offersubscription__exact=offer_subscription.pk,
 			period_from__lte=date,
 			period_to__gte=date,
 			invoice__member__exact=member.pk)
@@ -67,12 +66,12 @@ def create_member_invoice_for_a_period(member, date):
 			# Vérifie s'il s'agit de la première facture d'un abonnement,
 			# Alors facture en plus les frais de mise en service
 			invoicedetail_test_first = InvoiceDetail.objects.filter(
-				offer__exact=offer.pk,
+				offersubscription__exact=offer_subscription.pk,
 				invoice__member__exact=member.pk)
 			if not invoicedetail_test_first.exists():
 				invoice.details.create(label=offer.name + " - Frais de mise en service",
 					amount=offer.initial_fees,
-					offer=offer,
+					offersubscription=offer_subscription,
 					period_from=None,
 					period_to=None)
 
@@ -85,13 +84,14 @@ def create_member_invoice_for_a_period(member, date):
 					   relativedelta(days = +1))
 
 			# Recherche dans les factures déjà existantes de ce membre des items
-			# ayant cette offre pour lesquels la période de facturation
+			# ayant cet abonnement pour lesquels la période de facturation
 			# commence avant la fin de notre période de facturation actuelle
 			invoicedetail_test_after = InvoiceDetail.objects.filter(
-				offer__exact=offer.pk,
+				offersubscription__exact=offer_subscription.pk,
 				period_from__lte=period_to,
 				period_from__gte=period_from,
 				invoice__member__exact=member.pk)
+			
 			# Si une telle facture existe, récupère la date de début de
 			# facturation pour en faire la date de fin de facturation
 			if invoicedetail_test_after.exists():
@@ -103,7 +103,7 @@ def create_member_invoice_for_a_period(member, date):
 			#Ajout l'item de l'offre correspondant à l'abonnement à la facture			
 			invoice.details.create(label=offer.name,
 				amount=offer.period_fees,
-				offer=offer,
+				offersubscription=offer_subscription,
 				period_from=period_from,
 				period_to=period_to)
 

+ 97 - 0
coin/billing/migrations/0012_auto__add_field_invoicedetail_offersubscription.py

@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'InvoiceDetail.offersubscription'
+        db.add_column(u'billing_invoicedetail', 'offersubscription',
+                      self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['offers.OfferSubscription'], null=True, blank=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'InvoiceDetail.offersubscription'
+        db.delete_column(u'billing_invoicedetail', 'offersubscription_id')
+
+
+    models = {
+        u'billing.invoice': {
+            'Meta': {'object_name': 'Invoice'},
+            'date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today', 'null': 'True'}),
+            'date_due': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 2, 28, 0, 0)', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['members.Member']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'number': ('django.db.models.fields.CharField', [], {'default': "u'201402-183-523'", 'unique': 'True', 'max_length': '25'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'open'", 'max_length': '50'})
+        },
+        u'billing.invoicedetail': {
+            'Meta': {'object_name': 'InvoiceDetail'},
+            'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'invoice': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'details'", 'to': u"orm['billing.Invoice']"}),
+            'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'offer': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['offers.Offer']", 'null': 'True', 'blank': 'True'}),
+            'offersubscription': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['offers.OfferSubscription']", 'null': 'True', 'blank': 'True'}),
+            'period_from': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 2, 1, 0, 0)', 'null': 'True', 'blank': 'True'}),
+            'period_to': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 2, 28, 0, 0)', 'null': 'True', 'blank': 'True'}),
+            'quantity': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}),
+            'tax': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'null': 'True', 'max_digits': '4', 'decimal_places': '2'})
+        },
+        u'billing.payment': {
+            'Meta': {'object_name': 'Payment'},
+            'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2'}),
+            'date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['billing.Invoice']"}),
+            'payment_mean': ('django.db.models.fields.CharField', [], {'default': "'transfer'", 'max_length': '100', 'null': 'True'})
+        },
+        u'members.member': {
+            'Meta': {'object_name': 'Member'},
+            'address': ('django.db.models.fields.TextField', [], {}),
+            'city': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'country': ('django.db.models.fields.CharField', [], {'default': "'France'", 'max_length': '200'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '254'}),
+            'entry_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'home_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'ldap_cn': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'mobile_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            'organization_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'postal_code': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
+            'resign_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'non_adherent'", 'max_length': '50'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'personne_physique'", 'max_length': '20'})
+        },
+        u'offers.offer': {
+            'Meta': {'object_name': 'Offer'},
+            'billing_period': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'initial_fees': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'period_fees': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            'service': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offers.Service']"})
+        },
+        u'offers.offersubscription': {
+            'Meta': {'object_name': 'OfferSubscription'},
+            'commitment': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['members.Member']"}),
+            'offer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offers.Offer']"}),
+            'resign_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'subscription_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'})
+        },
+        u'offers.service': {
+            'Meta': {'object_name': 'Service'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        }
+    }
+
+    complete_apps = ['billing']

+ 96 - 0
coin/billing/migrations/0013_auto__del_field_invoicedetail_offer.py

@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Deleting field 'InvoiceDetail.offer'
+        db.delete_column(u'billing_invoicedetail', 'offer_id')
+
+
+    def backwards(self, orm):
+        # Adding field 'InvoiceDetail.offer'
+        db.add_column(u'billing_invoicedetail', 'offer',
+                      self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['offers.Offer'], null=True, blank=True),
+                      keep_default=False)
+
+
+    models = {
+        u'billing.invoice': {
+            'Meta': {'object_name': 'Invoice'},
+            'date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today', 'null': 'True'}),
+            'date_due': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 3, 31, 0, 0)', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['members.Member']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'number': ('django.db.models.fields.CharField', [], {'default': "u'201403-453-235'", 'unique': 'True', 'max_length': '25'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'open'", 'max_length': '50'})
+        },
+        u'billing.invoicedetail': {
+            'Meta': {'object_name': 'InvoiceDetail'},
+            'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'invoice': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'details'", 'to': u"orm['billing.Invoice']"}),
+            'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'offersubscription': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['offers.OfferSubscription']", 'null': 'True', 'blank': 'True'}),
+            'period_from': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 3, 1, 0, 0)', 'null': 'True', 'blank': 'True'}),
+            'period_to': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 3, 31, 0, 0)', 'null': 'True', 'blank': 'True'}),
+            'quantity': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}),
+            'tax': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'null': 'True', 'max_digits': '4', 'decimal_places': '2'})
+        },
+        u'billing.payment': {
+            'Meta': {'object_name': 'Payment'},
+            'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2'}),
+            'date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['billing.Invoice']"}),
+            'payment_mean': ('django.db.models.fields.CharField', [], {'default': "'transfer'", 'max_length': '100', 'null': 'True'})
+        },
+        u'members.member': {
+            'Meta': {'object_name': 'Member'},
+            'address': ('django.db.models.fields.TextField', [], {}),
+            'city': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'country': ('django.db.models.fields.CharField', [], {'default': "'France'", 'max_length': '200'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '254'}),
+            'entry_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'home_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'ldap_cn': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'mobile_phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}),
+            'organization_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'postal_code': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
+            'resign_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'status': ('django.db.models.fields.CharField', [], {'default': "'non_adherent'", 'max_length': '50'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'personne_physique'", 'max_length': '20'})
+        },
+        u'offers.offer': {
+            'Meta': {'object_name': 'Offer'},
+            'billing_period': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'initial_fees': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'period_fees': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            'service': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offers.Service']"})
+        },
+        u'offers.offersubscription': {
+            'Meta': {'object_name': 'OfferSubscription'},
+            'commitment': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'member': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['members.Member']"}),
+            'offer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offers.Offer']"}),
+            'resign_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'subscription_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'})
+        },
+        u'offers.service': {
+            'Meta': {'object_name': 'Service'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        }
+    }
+
+    complete_apps = ['billing']

+ 5 - 4
coin/billing/models.py

@@ -4,7 +4,7 @@ import calendar
 import random
 from decimal import Decimal
 from django.db import models
-from coin.offers.models import Offer
+from coin.offers.models import Offer, OfferSubscription
 from coin.members.models import Member
 
 
@@ -42,7 +42,7 @@ class Invoice(models.Model):
 
     def has_owner(self, uid):
       "Check if passed uid (ex gmajax) is owner of the invoice"
-      return self.member.ldap_cn == uid
+      return self.member and self.member.ldap_cn and self.member.ldap_cn == uid
 
     @staticmethod
     def next_invoice_number():
@@ -71,8 +71,9 @@ class InvoiceDetail(models.Model):
                               help_text='en %')
     invoice = models.ForeignKey(Invoice, verbose_name='Facture',
                                 related_name='details')
-    offer = models.ForeignKey(Offer, null=True, blank=True, default=None,
-                              verbose_name='Offre')
+    offersubscription = models.ForeignKey(OfferSubscription, null=True,
+                                          blank=True, default=None,
+                                          verbose_name='Abonnement')
     period_from = models.DateField(
         default=datetime.date(datetime.date.today().year,
                               datetime.date.today().month, 1),

+ 9 - 9
coin/billing/tests.py

@@ -35,14 +35,14 @@ class BillingInvoiceCreationTests(TestCase):
 		invoice = create_member_invoice_for_a_period(member, datetime.date(2014,1,1))
 		# La facture doit avoir les frais de mise en service
 		# Pour tester cela on tri par montant d'item décroissant.
-		# Comme les initial_fees sont plus élevées que les period_fees, il doit
-		# sortie en premier
+		# Comme dans l'offre créé, les initial_fees sont plus élevées que
+		# les period_fees, il doit sortir en premier
 		self.assertEqual(invoice.details.order_by('-amount').first().amount, 50)
 
 	def test_subscription_cant_be_charged_twice(self):
 		"""
-		Test qu'un abonnement ne peut pas être facturé deux fois pendant une
-		période
+		Test qu'un abonnement ne peut pas être facturé deux fois
+		(pas de chevauchement possible)
 		"""
 
 		# Créé une offre
@@ -66,19 +66,19 @@ class BillingInvoiceCreationTests(TestCase):
 		# Créé une facture
 		invoice = Invoice(member=member)
 		invoice.save()
-		# Créé une facturation pour cette offre pour la première période
+		# Créé une facturation pour cet abonnement pour la première période
 		# de janvier à mars
 		invoice.details.create(label=offer.name,
 				amount=offer.period_fees,
-				offer=offer,
+				offersubscription=subscription,
 				period_from=datetime.date(2014,1,1),
 				period_to=datetime.date(2014,3,31))
 
-		# Créé une facturation pour cette offre pour une seconde période
+		# Créé une facturation pour cet abonnement pour une seconde période
 		# de juin à aout
 		invoice.details.create(label=offer.name,
 				amount=offer.period_fees,
-				offer=offer,
+				offersubscription=subscription,
 				period_from=datetime.date(2014,6,1),
 				period_to=datetime.date(2014,8,31))
 
@@ -134,7 +134,7 @@ class BillingPDFTests(TestCase):
 	def test_that_only_owner_of_invoice_can_download_it_as_pdf(self):
 		"""
 		Test qu'une facture ne peut pas être téléchargée par quelqu'un qui n'en
-		ait pas le propriétaire.
+		est pas le propriétaire.
 		Test qu'une erreur 403 est bien retournée en cas de tentative
 		infructueuse
 		"""

+ 73 - 0
coin/filtering_queryset.py

@@ -0,0 +1,73 @@
+from django import forms
+from django.core.exceptions import ObjectDoesNotExist
+import logging
+
+logger = logging.getLogger(__name__)
+
+class LimitedAdminInlineMixin(object):
+    """
+    InlineAdmin mixin limiting the selection of related items according to
+    criteria which can depend on the current parent object being edited.
+
+    A typical use case would be selecting a subset of related items from
+    other inlines, ie. images, to have some relation to other inlines.
+
+    Use as follows::
+
+        class MyInline(LimitedAdminInlineMixin, admin.TabularInline):
+            def get_filters(self, obj):
+                return (('<field_name>', dict(<filters>)),)
+
+    """
+
+    @staticmethod
+    def limit_inline_choices(formset, field, empty=False, **filters):
+        """
+        This function fetches the queryset with available choices for a given
+        `field` and filters it based on the criteria specified in filters,
+        unless `empty=True`. In this case, no choices will be made available.
+        """
+        try:        
+            assert formset.form.base_fields.has_key(field)
+
+            qs = formset.form.base_fields[field].queryset
+            if empty:
+                logger.debug('Limiting the queryset to none')
+                formset.form.base_fields[field].queryset = qs.none()
+            else:
+                qs = qs.filter(**filters)
+                logger.debug('Limiting queryset for formset to: %s', qs)
+
+                formset.form.base_fields[field].queryset = qs
+        except:
+            pass
+
+    def get_formset(self, request, obj=None, **kwargs):
+        """
+        Make sure we can only select variations that relate to the current
+        item.
+        """
+        formset = \
+            super(LimitedAdminInlineMixin, self).get_formset(request,
+                                                             obj,
+                                                             **kwargs)
+        for (field, filters) in self.get_filters(obj):
+            if obj and filters:
+                self.limit_inline_choices(formset, field, **filters)
+            else:
+                self.limit_inline_choices(formset, field, empty=True)
+        
+        return formset
+
+    def get_filters(self, obj):
+        """
+        Return filters for the specified fields. Filters should be in the
+        following format::
+
+            (('field_name', {'categories': obj}), ...)
+
+        For this to work, we should either override `get_filters` in a
+        subclass or define a `filters` property with the same syntax as this
+        one.
+        """
+        return getattr(self, 'filters', ())

+ 10 - 4
coin/members/models.py

@@ -8,13 +8,13 @@ import unicodedata
 import string
 import datetime
 from django.db import models
-from ldapdb.models.fields import CharField, IntegerField, ListField
+from django.db.models import Q
 from django.db.models.signals import post_save, pre_save, post_delete
 from django.dispatch import receiver
-from south.modelsinspector import add_ignored_fields
 from django.core import exceptions
-import logging
-
+from ldapdb.models.fields import CharField, IntegerField, ListField
+from south.modelsinspector import add_ignored_fields
+from coin.offers.models import OfferSubscription
 
 class Member(models.Model):
 
@@ -78,6 +78,12 @@ class Member(models.Model):
         ldap_user.password = new_password
         ldap_user.save()
 
+    def get_active_subscriptions(self, date=datetime.date.today()):
+        return OfferSubscription.objects.filter(
+            Q(member__exact=self.pk),
+            Q(subscription_date__lte=date),
+            Q(resign_date__isnull=True) | Q(resign_date__gte=date))
+
     class Meta:
         verbose_name = 'membre'
 

+ 0 - 8
coin/offers/models.py

@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 import datetime
 from django.db import models
-from django.db.models import Q
 
 
 class Service(models.Model):
@@ -58,12 +57,5 @@ class OfferSubscription(models.Model):
         return u'%s - %s - %s' % (self.member, self.offer.name,
                                    self.subscription_date)
 
-    @staticmethod
-    def get_member_offer_subscriptions(member, date=datetime.date.today()):
-      return OfferSubscription.objects.filter(
-        Q(member__exact=member.pk),
-        Q(subscription_date__lte=date),
-        Q(resign_date__isnull=True) | Q(resign_date__gte=date))
-
     class Meta:
         verbose_name = 'abonnement'