Skip to content

Commit

Permalink
corp renew: add donation allocation and option to create a separate i…
Browse files Browse the repository at this point in the history
…nvoice for donation
  • Loading branch information
jennyq committed Apr 11, 2024
1 parent 725a585 commit de5871c
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 23 deletions.
22 changes: 21 additions & 1 deletion tendenci/apps/corporate_memberships/forms.py
Expand Up @@ -14,7 +14,7 @@
from django.db.models import Q
from django.core.files.storage import default_storage
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
#from django.conf import settings

# from captcha.fields import CaptchaField
#from tendenci.apps.base.forms import SimpleMathField
Expand Down Expand Up @@ -51,6 +51,7 @@
from tendenci.apps.products.models import Product, Category as ProductCategory
from tendenci.apps.emails.models import Email
from tendenci.apps.site_settings.utils import get_setting
from tendenci.apps.entities.models import Entity


fs = FileSystemStorage(location=UPLOAD_ROOT)
Expand Down Expand Up @@ -726,6 +727,7 @@ class Meta:
model = CorpMembership
fields = ('corporate_membership_type',
'payment_method',
'donate_to_entity'
)

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -753,6 +755,18 @@ def __init__(self, *args, **kwargs):
self.fields['donation_option_value'].label = self.corpmembership_app.donation_label
self.fields['donation_option_value'].widget = DonationOptionAmountWidget(attrs={},
default_amount=self.corpmembership_app.donation_default_amount)
entity_qs = Entity.objects.filter(show_for_donation=True)
if entity_qs.exists():
self.fields['donate_to_entity'].widget = forms.RadioSelect()
self.fields['donate_to_entity'].queryset = entity_qs
self.fields['donate_to_entity'].empty_label = None
self.fields['donate_to_entity'].label = _('Donate to')
if entity_qs.count() == 1:
self.fields['donate_to_entity'].initial = entity_qs[0]
else:
del self.fields['donate_to_entity']
else:
del self.fields['donate_to_entity']

#if not self.instance.corporate_membership_type.membership_type.renewal_price:
self.fields['select_all_members'].initial = False
Expand All @@ -777,6 +791,12 @@ def clean(self):
raise forms.ValidationError(
_("You've selected {count} individual members, but the maximum allowed is {cap}.".format(count=count_members, cap=cmt.membership_cap)) )

if 'donation_option_value' in self.cleaned_data \
and 'donate_to_entity' in self.cleaned_data:
if not self.cleaned_data['donate_to_entity']:
raise forms.ValidationError({'donate_to_entity': _("Please select where we should allocate your donation.")
})

return cleaned_data

def clean_donation_option_value(self):
Expand Down
@@ -0,0 +1,26 @@
# Generated by Django 3.2.23 on 2024-04-11 11:48

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('donations', '0005_auto_20240409_1733'),
('entities', '0005_entity_show_for_donation'),
('corporate_memberships', '0029_broadcastemail'),
]

operations = [
migrations.AddField(
model_name='corpmembership',
name='donate_to_entity',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='entities.entity'),
),
migrations.AddField(
model_name='corpmembership',
name='donation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='donations.donation'),
),
]
39 changes: 38 additions & 1 deletion tendenci/apps/corporate_memberships/models.py
Expand Up @@ -19,6 +19,7 @@
from django.db.models import Q
from django.db.models.signals import post_delete
from django.template.defaultfilters import slugify
from django.contrib.contenttypes.models import ContentType

#from django.contrib.contenttypes.models import ContentType
from tendenci.libs.tinymce import models as tinymce_models
Expand Down Expand Up @@ -65,6 +66,7 @@
from tendenci.apps.user_groups.models import Group
from tendenci.libs.utils import python_executable
from tendenci.apps.products.models import Product
from tendenci.apps.donations.models import Donation


FIELD_CHOICES = (
Expand Down Expand Up @@ -622,6 +624,8 @@ class CorpMembership(TendenciBaseModel):

invoice = models.ForeignKey(Invoice, blank=True, null=True, on_delete=models.SET_NULL)
donation_amount = models.DecimalField(max_digits=15, decimal_places=2, blank=True, default=0)
donate_to_entity = models.ForeignKey(Entity, blank=True, null=True, on_delete=models.SET_NULL)
donation = models.ForeignKey(Donation, blank=True, null=True, on_delete=models.SET_NULL)

anonymous_creator = models.ForeignKey('Creator', null=True, on_delete=models.SET_NULL)
admin_notes = models.TextField(_('Admin notes'),
Expand Down Expand Up @@ -670,6 +674,39 @@ def get_absolute_url(self):
"""
return reverse('corpmembership.view', args=[self.pk])

def donation_add(self, request_user):
"""
Add a donation record, along with its invoice.
"""
if self.donation_amount and \
get_setting('module', 'corporate_memberships', 'donationsepinv'):
if not self.donation:
donation = Donation(user=request_user,
first_name=request_user.first_name,
last_name=request_user.last_name,
email=request_user.email,
phone=self.corp_profile.phone,
company=self.corp_profile.name,
address=self.corp_profile.address,
address2=self.corp_profile.address2,
city=self.corp_profile.city,
state=self.corp_profile.state,
zip_code=self.corp_profile.zip,
donation_amount=self.donation_amount,
donate_to_entity=self.donate_to_entity,
object_type=ContentType.objects.get_for_model(self),
object_id=self.id,
comments=f'Made from corp membership {self.corp_profile.name} (ID: {self.id}) renewal')
donation.save(user=request_user)
donation.inv_add(request_user)
self.donation = donation
self.save(update_fields=['donation'])

def donation_not_paid(self):
if self.donation and self.donation.invoice:
return self.donation.invoice.balance > 0
return False

@property
def group(self):
return self.corporate_membership_type.membership_type.group
Expand Down Expand Up @@ -966,7 +1003,7 @@ def copy(self):
field_names = [field.name for field in self.__class__._meta.fields]
ignore_fields = ['id', 'renewal', 'renew_dt', 'status', 'donation_amount',
'status_detail', 'approved', 'approved_denied_dt',
'approved_denied_user', 'anonymous_creator']
'approved_denied_user', 'anonymous_creator', 'donation']
for field in ignore_fields:
field_names.remove(field)

Expand Down
17 changes: 17 additions & 0 deletions tendenci/apps/corporate_memberships/settings.json
Expand Up @@ -236,5 +236,22 @@
"scope_category": "corporate_memberships",
"store": true,
"updated_by": ""
},
{
"default_value": "false",
"description": "If donation is enabled, a separate invoice will be created for donation on corp memberships join/renewal. Default False.",
"data_type": "boolean",
"update_dt": "2024-04-09 20:50:00",
"input_type": "select",
"client_editable": true,
"name": "donationsepinv",
"value": "false",
"label": "Use A Separate Invoice for Donation",
"input_value": "true,false",
"parent_id": 0,
"scope": "module",
"scope_category": "corporate_memberships",
"store": true,
"updated_by": ""
}
]
12 changes: 7 additions & 5 deletions tendenci/apps/corporate_memberships/utils.py
Expand Up @@ -268,6 +268,7 @@ def corp_memb_inv_add(user, corp_memb, app=None, **kwargs):
corp_profile = corp_memb.corp_profile
renewal = kwargs.get('renewal', False)
renewal_total = kwargs.get('renewal_total', 0)
include_donation = kwargs.get('include_donation', True)
if not corp_memb.invoice or renewal:
inv = Invoice()
inv.entity = corp_profile.entity
Expand Down Expand Up @@ -333,11 +334,12 @@ def corp_memb_inv_add(user, corp_memb, app=None, **kwargs):
inv.balance = total

# Check for donation
donation_amount = kwargs.get('donation_amount', None)
if donation_amount:
inv.subtotal += donation_amount
inv.total += donation_amount
inv.balance += donation_amount
if include_donation:
donation_amount = kwargs.get('donation_amount', None)
if donation_amount:
inv.subtotal += donation_amount
inv.total += donation_amount
inv.balance += donation_amount

inv.estimate = True
inv.status_detail = 'estimate'
Expand Down
12 changes: 7 additions & 5 deletions tendenci/apps/corporate_memberships/views.py
Expand Up @@ -231,10 +231,7 @@ def free_passes_list(request,
def corp_members_donated(request,
template_name='corporate_memberships/reports/corp_members_donated.html'):
corp_members = CorpMembership.objects.filter(donation_amount__gt=0
).values('id',
'corp_profile__name', 'donation_amount',
'invoice', 'create_dt', 'status_detail'
).order_by('-donation_amount', 'corp_profile__name')
).order_by('-donation_amount', 'corp_profile__name')

return render_to_resp(request=request, template_name=template_name,
context={'corp_members': corp_members,})
Expand Down Expand Up @@ -1272,7 +1269,8 @@ def corp_renew(request, id,
indiv_renewal_price * count_members

opt_d = {'renewal': True,
'renewal_total': renewal_total}
'renewal_total': renewal_total,
'include_donation': not get_setting('module', 'corporate_memberships', 'donationsepinv')}
if corpmembership_app.donation_enabled:
# check for donation
donation_option, donation_amount = form.cleaned_data.get('donation_option_value', (None, None))
Expand All @@ -1289,6 +1287,10 @@ def corp_renew(request, id,
new_corp_membership,
**opt_d)
new_corp_membership.invoice = inv
if new_corp_membership.donation_amount:
# check if should create a separate invoice for donation
if not opt_d['include_donation']:
new_corp_membership.donation_add(request.user)
new_corp_membership.save()

# assign object permissions
Expand Down
25 changes: 25 additions & 0 deletions tendenci/apps/donations/migrations/0005_auto_20240409_1733.py
@@ -0,0 +1,25 @@
# Generated by Django 3.2.23 on 2024-04-09 17:33

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('donations', '0004_donation_donate_to_entity'),
]

operations = [
migrations.AddField(
model_name='donation',
name='object_id',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='donation',
name='object_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
),
]
72 changes: 68 additions & 4 deletions tendenci/apps/donations/models.py
@@ -1,7 +1,11 @@
import uuid
from datetime import datetime
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

from tendenci.apps.invoices.models import Invoice
from tendenci.apps.entities.models import Entity
from tendenci.apps.donations.managers import DonationManager
Expand All @@ -28,13 +32,16 @@ class Donation(models.Model):
payment_method = models.CharField(max_length=50, default='cc')
invoice = models.ForeignKey(Invoice, blank=True, null=True, on_delete=models.SET_NULL)
donate_to_entity = models.ForeignKey(Entity, blank=True, null=True, on_delete=models.SET_NULL)
object_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE)
object_id = models.IntegerField(default=0, blank=True, null=True)
create_dt = models.DateTimeField(auto_now_add=True)
creator = models.ForeignKey(User, null=True, related_name="donation_creator", on_delete=models.SET_NULL)
creator_username = models.CharField(max_length=150, null=True)
owner = models.ForeignKey(User, null=True, related_name="donation_owner", on_delete=models.SET_NULL)
owner_username = models.CharField(max_length=150, null=True)
status_detail = models.CharField(max_length=50, default='estimate')
status = models.BooleanField(default=True, null=True)
from_object = GenericForeignKey('object_type', 'object_id')

objects = DonationManager()

Expand Down Expand Up @@ -70,10 +77,10 @@ def get_payment_description(self, inv):
The description will be sent to payment gateway and displayed on invoice.
If not supplied, the default description will be generated.
"""
return 'Tendenci Invoice %d Payment for Donation %d' % (
inv.id,
inv.object_id,
)
description = f'Tendenci Invoice {inv.id} Payment for Donation {inv.object_id}'
if self.from_object:
description += f" from {str(self.from_object)}"
return description

def make_acct_entries(self, user, inv, amount, **kwargs):
"""
Expand Down Expand Up @@ -104,6 +111,10 @@ def auto_update_paid_object(self, request, payment):
"""
Update the object after online payment is received.
"""
if self.from_object:
# skip the notification if donation is made from corp membership renewal
return

# email to admin
try:
from tendenci.apps.notifications import models as notification
Expand All @@ -120,3 +131,56 @@ def auto_update_paid_object(self, request, payment):
'request': request,
}
notification.send_emails(recipients,'donation_added', extra_context)

def is_paid(self):
return self.invoice and self.invoice.balance <= 0

def inv_add(self, user, **kwargs):
inv = Invoice()
inv.title = "Donation Invoice"
inv.bill_to = self.first_name + ' ' + self.last_name
inv.bill_to_first_name = self.first_name
inv.bill_to_last_name = self.last_name
inv.bill_to_company = self.company
inv.bill_to_address = self.address
inv.bill_to_city = self.city
inv.bill_to_state = self.state
inv.bill_to_zip_code = self.zip_code
inv.bill_to_country = self.country
inv.bill_to_phone = self.phone
inv.bill_to_email = self.email
inv.ship_to = self.first_name + ' ' + self.last_name
inv.ship_to_first_name = self.first_name
inv.ship_to_last_name = self.last_name
inv.ship_to_company = self.company
inv.ship_to_address = self.address
inv.ship_to_city = self.city
inv.ship_to_state = self.state
inv.ship_to_zip_code = self.zip_code
inv.ship_to_country = self.country
inv.ship_to_phone = self.phone
#self.ship_to_fax = make_payment.fax
inv.ship_to_email = self.email
inv.terms = "Due on Receipt"
inv.due_date = datetime.now()
inv.ship_date = datetime.now()
inv.message = 'Thank You.'
inv.status = True

if self.donate_to_entity:
inv.entity = self.donate_to_entity

inv.estimate = True
inv.status_detail = 'tendered'
inv.object_type = ContentType.objects.get(app_label=self._meta.app_label,
model=self._meta.model_name)
inv.object_id = self.id
inv.subtotal = self.donation_amount
inv.total = self.donation_amount
inv.balance = self.donation_amount

inv.save(user)
self.invoice = inv
self.save()

return inv
4 changes: 4 additions & 0 deletions tendenci/apps/invoices/models.py
Expand Up @@ -720,6 +720,10 @@ def object_display(self, obj=None):
return obj.membership_type
return obj

def obj_donation(self):
obj = self.get_object()
return hasattr(obj, 'donation') and obj.donation

def cancel_registration(self, request, refund=False):
"""Cancel registration for this invoice"""
if not self.registration:
Expand Down

0 comments on commit de5871c

Please sign in to comment.