534 lines
16 KiB
Python
534 lines
16 KiB
Python
# -*- coding: utf-8; -*-
|
|
################################################################################
|
|
#
|
|
# Rattail -- Retail Software Framework
|
|
# Copyright © 2010-2023 Lance Edgar
|
|
#
|
|
# This file is part of Rattail.
|
|
#
|
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation, either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
################################################################################
|
|
"""
|
|
Member Views
|
|
"""
|
|
|
|
from collections import OrderedDict
|
|
|
|
import sqlalchemy as sa
|
|
|
|
from rattail.db import model
|
|
from rattail.db.model import MembershipType, Member, MemberEquityPayment
|
|
|
|
from deform import widget as dfwidget
|
|
from webhelpers2.html import tags
|
|
|
|
from tailbone import grids, forms
|
|
from tailbone.views import MasterView
|
|
|
|
|
|
class MembershipTypeView(MasterView):
|
|
"""
|
|
Master view for Membership Types
|
|
"""
|
|
model_class = MembershipType
|
|
route_prefix = 'membership_types'
|
|
url_prefix = '/membership-types'
|
|
has_versions = True
|
|
|
|
labels = {
|
|
'id': "ID",
|
|
}
|
|
|
|
grid_columns = [
|
|
'number',
|
|
'name',
|
|
]
|
|
|
|
has_rows = True
|
|
model_row_class = Member
|
|
rows_title = "Members"
|
|
|
|
row_grid_columns = [
|
|
'_member_key_',
|
|
'person',
|
|
'active',
|
|
'equity_current',
|
|
'equity_total',
|
|
'joined',
|
|
'withdrew',
|
|
]
|
|
|
|
def configure_grid(self, g):
|
|
super().configure_grid(g)
|
|
|
|
g.set_sort_defaults('number')
|
|
|
|
g.set_link('number')
|
|
g.set_link('name')
|
|
|
|
def get_row_data(self, memtype):
|
|
model = self.model
|
|
return self.Session.query(model.Member)\
|
|
.filter(model.Member.membership_type == memtype)
|
|
|
|
def get_parent(self, member):
|
|
return member.membership_type
|
|
|
|
def configure_row_grid(self, g):
|
|
super().configure_row_grid(g)
|
|
|
|
g.filters['active'].default_active = True
|
|
g.filters['active'].default_verb = 'is_true'
|
|
|
|
g.set_link('person')
|
|
|
|
def row_view_action_url(self, member, i):
|
|
return self.request.route_url('members.view', uuid=member.uuid)
|
|
|
|
|
|
class MemberView(MasterView):
|
|
"""
|
|
Master view for the Member class.
|
|
"""
|
|
model_class = model.Member
|
|
is_contact = True
|
|
touchable = True
|
|
has_versions = True
|
|
configurable = True
|
|
supports_autocomplete = True
|
|
|
|
labels = {
|
|
'id': "ID",
|
|
'person': "Account Holder",
|
|
}
|
|
|
|
grid_columns = [
|
|
'_member_key_',
|
|
'person',
|
|
'membership_type',
|
|
'active',
|
|
'equity_current',
|
|
'joined',
|
|
'withdrew',
|
|
]
|
|
|
|
form_fields = [
|
|
'_member_key_',
|
|
'person',
|
|
'customer',
|
|
'default_email',
|
|
'default_phone',
|
|
'membership_type',
|
|
'active',
|
|
'equity_total',
|
|
'equity_current',
|
|
'equity_payment_due',
|
|
'joined',
|
|
'withdrew',
|
|
]
|
|
|
|
has_rows = True
|
|
model_row_class = MemberEquityPayment
|
|
rows_title = "Equity Payments"
|
|
|
|
row_grid_columns = [
|
|
'received',
|
|
'amount',
|
|
'description',
|
|
'source',
|
|
'transaction_identifier',
|
|
]
|
|
|
|
def should_expose_quickie_search(self):
|
|
if self.expose_quickie_search:
|
|
return True
|
|
app = self.get_rattail_app()
|
|
return app.get_people_handler().should_expose_quickie_search()
|
|
|
|
def get_quickie_perm(self):
|
|
return 'people.quickie'
|
|
|
|
def get_quickie_url(self):
|
|
return self.request.route_url('people.quickie')
|
|
|
|
def get_quickie_placeholder(self):
|
|
app = self.get_rattail_app()
|
|
return app.get_people_handler().get_quickie_search_placeholder()
|
|
|
|
def configure_grid(self, g):
|
|
super(MemberView, self).configure_grid(g)
|
|
route_prefix = self.get_route_prefix()
|
|
model = self.model
|
|
|
|
# member key
|
|
field = self.get_member_key_field()
|
|
g.filters[field].default_active = True
|
|
g.filters[field].default_verb = 'equal'
|
|
g.set_sort_defaults(field)
|
|
g.set_link(field)
|
|
|
|
g.set_joiner('person', lambda q: q.outerjoin(model.Person))
|
|
g.set_sorter('person', model.Person.display_name)
|
|
g.set_filter('person', model.Person.display_name)
|
|
|
|
g.set_joiner('customer', lambda q: q.outerjoin(model.Customer))
|
|
g.set_sorter('customer', model.Customer.name)
|
|
g.set_filter('customer', model.Customer.name)
|
|
|
|
g.filters['active'].default_active = True
|
|
g.filters['active'].default_verb = 'is_true'
|
|
|
|
# phone
|
|
g.set_joiner('phone', lambda q: q.outerjoin(model.MemberPhoneNumber, sa.and_(
|
|
model.MemberPhoneNumber.parent_uuid == model.Member.uuid,
|
|
model.MemberPhoneNumber.preference == 1)))
|
|
g.sorters['phone'] = lambda q, d: q.order_by(getattr(model.MemberPhoneNumber.number, d)())
|
|
g.set_filter('phone', model.MemberPhoneNumber.number,
|
|
factory=grids.filters.AlchemyPhoneNumberFilter)
|
|
g.set_label('phone', "Phone Number")
|
|
|
|
# email
|
|
g.set_joiner('email', lambda q: q.outerjoin(model.MemberEmailAddress, sa.and_(
|
|
model.MemberEmailAddress.parent_uuid == model.Member.uuid,
|
|
model.MemberEmailAddress.preference == 1)))
|
|
g.sorters['email'] = lambda q, d: q.order_by(getattr(model.MemberEmailAddress.address, d)())
|
|
g.set_filter('email', model.MemberEmailAddress.address)
|
|
g.set_label('email', "Email Address")
|
|
|
|
# membership_type
|
|
g.set_joiner('membership_type', lambda q: q.outerjoin(model.MembershipType))
|
|
g.set_sorter('membership_type', model.MembershipType.name)
|
|
g.set_filter('membership_type', model.MembershipType.name,
|
|
label="Membership Type Name")
|
|
|
|
if (self.request.has_perm('people.view_profile')
|
|
and self.should_link_straight_to_profile()):
|
|
|
|
# add View Raw action
|
|
url = lambda r, i: self.request.route_url(
|
|
f'{route_prefix}.view', **self.get_action_route_kwargs(r))
|
|
# nb. insert to slot 1, just after normal View action
|
|
g.main_actions.insert(1, self.make_action(
|
|
'view_raw', url=url, icon='eye'))
|
|
|
|
g.set_link('person')
|
|
g.set_link('customer')
|
|
|
|
def default_view_url(self):
|
|
if (self.request.has_perm('people.view_profile')
|
|
and self.should_link_straight_to_profile()):
|
|
app = self.get_rattail_app()
|
|
|
|
def url(member, i):
|
|
person = app.get_person(member)
|
|
if person:
|
|
return self.request.route_url(
|
|
'people.view_profile', uuid=person.uuid,
|
|
_anchor='member')
|
|
return self.get_action_url('view', member)
|
|
|
|
return url
|
|
|
|
return super().default_view_url()
|
|
|
|
def should_link_straight_to_profile(self):
|
|
return self.rattail_config.getbool('rattail',
|
|
'members.straight_to_profile',
|
|
default=False)
|
|
|
|
def grid_extra_class(self, member, i):
|
|
if not member.active:
|
|
return 'warning'
|
|
if member.equity_current is False:
|
|
return 'notice'
|
|
|
|
def configure_form(self, f):
|
|
super().configure_form(f)
|
|
member = f.model_instance
|
|
|
|
# date fields
|
|
f.set_type('joined', 'date_jquery')
|
|
f.set_type('withdrew', 'date_jquery')
|
|
|
|
# equity fields
|
|
f.set_renderer('equity_total', self.render_equity_total)
|
|
f.set_type('equity_payment_due', 'date_jquery')
|
|
f.set_type('equity_last_paid', 'date_jquery')
|
|
|
|
# person
|
|
if self.creating or self.editing:
|
|
if 'person' in f.fields:
|
|
f.replace('person', 'person_uuid')
|
|
people = self.Session.query(model.Person)\
|
|
.order_by(model.Person.display_name)
|
|
values = [(p.uuid, str(p))
|
|
for p in people]
|
|
require = False
|
|
if not require:
|
|
values.insert(0, ('', "(none)"))
|
|
f.set_widget('person_uuid', dfwidget.SelectWidget(values=values))
|
|
f.set_label('person_uuid', "Person")
|
|
else:
|
|
f.set_readonly('person')
|
|
f.set_renderer('person', self.render_person)
|
|
|
|
# customer
|
|
if self.creating or self.editing:
|
|
if 'customer' in f.fields:
|
|
f.replace('customer', 'customer_uuid')
|
|
customers = self.Session.query(model.Customer)\
|
|
.order_by(model.Customer.name)
|
|
values = [(c.uuid, str(c))
|
|
for c in customers]
|
|
require = False
|
|
if not require:
|
|
values.insert(0, ('', "(none)"))
|
|
f.set_widget('customer_uuid', dfwidget.SelectWidget(values=values))
|
|
f.set_label('customer_uuid', "Customer")
|
|
else:
|
|
f.set_readonly('customer')
|
|
f.set_renderer('customer', self.render_customer)
|
|
|
|
# default_email
|
|
f.set_renderer('default_email', self.render_default_email)
|
|
if not self.creating and member.emails:
|
|
f.set_default('default_email', member.emails[0].address)
|
|
|
|
# default_phone
|
|
f.set_renderer('default_phone', self.render_default_phone)
|
|
if not self.creating and member.phones:
|
|
f.set_default('default_phone', member.phones[0].number)
|
|
|
|
# membership_type
|
|
f.set_renderer('membership_type', self.render_membership_type)
|
|
|
|
if self.creating:
|
|
f.remove_fields(
|
|
'equity_total',
|
|
'equity_last_paid',
|
|
'equity_payment_credit',
|
|
'withdrew',
|
|
)
|
|
|
|
def render_equity_total(self, member, field):
|
|
app = self.get_rattail_app()
|
|
total = sum([payment.amount for payment in member.equity_payments])
|
|
return app.render_currency(total)
|
|
|
|
def template_kwargs_view(self, **kwargs):
|
|
kwargs = super().template_kwargs_view(**kwargs)
|
|
app = self.get_rattail_app()
|
|
member = kwargs['instance']
|
|
|
|
people = OrderedDict()
|
|
person = app.get_person(member)
|
|
if person:
|
|
people.setdefault(person.uuid, person)
|
|
customer = app.get_customer(member)
|
|
if customer:
|
|
person = app.get_person(customer)
|
|
if person:
|
|
people.setdefault(person.uuid, person)
|
|
kwargs['show_profiles_people'] = list(people.values())
|
|
|
|
return kwargs
|
|
|
|
def render_default_email(self, member, field):
|
|
if member.emails:
|
|
return member.emails[0].address
|
|
|
|
def render_default_phone(self, member, field):
|
|
if member.phones:
|
|
return member.phones[0].number
|
|
|
|
def render_membership_type(self, member, field):
|
|
memtype = getattr(member, field)
|
|
if not memtype:
|
|
return
|
|
text = str(memtype)
|
|
url = self.request.route_url('membership_types.view', uuid=memtype.uuid)
|
|
return tags.link_to(text, url)
|
|
|
|
def get_row_data(self, member):
|
|
model = self.model
|
|
return self.Session.query(model.MemberEquityPayment)\
|
|
.filter(model.MemberEquityPayment.member == member)
|
|
|
|
def get_parent(self, payment):
|
|
return payment.member
|
|
|
|
def configure_row_grid(self, g):
|
|
super().configure_row_grid(g)
|
|
|
|
g.set_type('amount', 'currency')
|
|
|
|
g.set_sort_defaults('received', 'desc')
|
|
|
|
def row_view_action_url(self, payment, i):
|
|
return self.request.route_url('member_equity_payments.view',
|
|
uuid=payment.uuid)
|
|
|
|
def configure_get_simple_settings(self):
|
|
return [
|
|
|
|
# General
|
|
{'section': 'rattail',
|
|
'option': 'members.key_field'},
|
|
{'section': 'rattail',
|
|
'option': 'members.key_label'},
|
|
{'section': 'rattail',
|
|
'option': 'members.straight_to_profile',
|
|
'type': bool},
|
|
|
|
# Relationships
|
|
{'section': 'rattail',
|
|
'option': 'members.max_one_per_person',
|
|
'type': bool},
|
|
]
|
|
|
|
|
|
class MemberEquityPaymentView(MasterView):
|
|
"""
|
|
Master view for the MemberEquityPayment class.
|
|
"""
|
|
model_class = model.MemberEquityPayment
|
|
route_prefix = 'member_equity_payments'
|
|
url_prefix = '/member-equity-payments'
|
|
supports_grid_totals = True
|
|
has_versions = True
|
|
|
|
grid_columns = [
|
|
'received',
|
|
'_member_key_',
|
|
'member',
|
|
'amount',
|
|
'description',
|
|
'source',
|
|
'transaction_identifier',
|
|
]
|
|
|
|
form_fields = [
|
|
'_member_key_',
|
|
'member',
|
|
'amount',
|
|
'received',
|
|
'description',
|
|
'source',
|
|
'transaction_identifier',
|
|
]
|
|
|
|
def query(self, session):
|
|
query = super().query(session)
|
|
model = self.model
|
|
|
|
query = query.join(model.Member)
|
|
|
|
return query
|
|
|
|
def configure_grid(self, g):
|
|
super().configure_grid(g)
|
|
model = self.model
|
|
|
|
# member_key
|
|
field = self.get_member_key_field()
|
|
attr = getattr(model.Member, field)
|
|
g.set_renderer(field, self.render_member_key)
|
|
g.set_filter(field, attr,
|
|
label=self.get_member_key_label(),
|
|
default_active=True)
|
|
g.set_sorter(field, attr)
|
|
|
|
# member (name)
|
|
g.set_joiner('member', lambda q: q.outerjoin(model.Person))
|
|
g.set_sorter('member', model.Person.display_name)
|
|
g.set_link('member')
|
|
g.set_filter('member', model.Person.display_name,
|
|
label="Member Name")
|
|
|
|
g.set_type('amount', 'currency')
|
|
|
|
g.set_sort_defaults('received', 'desc')
|
|
g.set_link('received')
|
|
|
|
# description
|
|
g.set_link('description')
|
|
|
|
g.set_link('transaction_identifier')
|
|
|
|
def render_member_key(self, payment, field):
|
|
key = getattr(payment.member, field)
|
|
return key
|
|
|
|
def fetch_grid_totals(self):
|
|
app = self.get_rattail_app()
|
|
results = self.get_effective_data()
|
|
total = sum([payment.amount for payment in results])
|
|
return {'totals_display': app.render_currency(total)}
|
|
|
|
def configure_form(self, f):
|
|
super().configure_form(f)
|
|
model = self.model
|
|
payment = f.model_instance
|
|
|
|
# member_key
|
|
field = self.get_member_key_field()
|
|
f.set_renderer(field, self.render_member_key)
|
|
|
|
# member
|
|
if self.creating:
|
|
f.replace('member', 'member_uuid')
|
|
member_display = ""
|
|
if self.request.method == 'POST':
|
|
if self.request.POST.get('member_uuid'):
|
|
member = self.Session.get(model.Member,
|
|
self.request.POST['member_uuid'])
|
|
if member:
|
|
member_display = str(member)
|
|
elif self.editing:
|
|
member_display = str(payment.member or '')
|
|
members_url = self.request.route_url('members.autocomplete')
|
|
f.set_widget('member_uuid', forms.widgets.JQueryAutocompleteWidget(
|
|
field_display=member_display, service_url=members_url))
|
|
f.set_label('member_uuid', "Member")
|
|
else:
|
|
f.set_readonly('member')
|
|
f.set_renderer('member', self.render_member)
|
|
|
|
# amount
|
|
f.set_type('amount', 'currency')
|
|
|
|
# received
|
|
if self.creating:
|
|
f.set_type('received', 'date_jquery')
|
|
else:
|
|
f.set_readonly('received')
|
|
|
|
|
|
def defaults(config, **kwargs):
|
|
base = globals()
|
|
|
|
MembershipTypeView = kwargs.get('MembershipTypeView', base['MembershipTypeView'])
|
|
MembershipTypeView.defaults(config)
|
|
|
|
MemberView = kwargs.get('MemberView', base['MemberView'])
|
|
MemberView.defaults(config)
|
|
|
|
MemberEquityPaymentView = kwargs.get('MemberEquityPaymentView', base['MemberEquityPaymentView'])
|
|
MemberEquityPaymentView.defaults(config)
|
|
|
|
|
|
def includeme(config):
|
|
defaults(config)
|