Add basic support for exposing Customer.shoppers

now there is a Shoppers field when viewing a Customer, unless
configured otherwise

also tweaked some logic for navigating Customer/Person relationships,
to handle implications of Shoppers being (maybe) present
This commit is contained in:
Lance Edgar 2023-06-07 16:27:10 -05:00
parent afd5c3a5fd
commit 3fde80f991
5 changed files with 201 additions and 46 deletions

View file

@ -26,12 +26,30 @@
</b-field>
<b-field message="If not set, customer chooser is an autocomplete field.">
<b-field message="Set this to show the Shoppers field when viewing a Customer record.">
<b-checkbox name="rattail.customers.expose_shoppers"
v-model="simpleSettings['rattail.customers.expose_shoppers']"
native-value="true"
@input="settingsNeedSaved = true">
Show the Shoppers field
</b-checkbox>
</b-field>
<b-field message="Set this to show the People field when viewing a Customer record.">
<b-checkbox name="rattail.customers.expose_people"
v-model="simpleSettings['rattail.customers.expose_people']"
native-value="true"
@input="settingsNeedSaved = true">
Show the People field
</b-checkbox>
</b-field>
<b-field message="If not set, Customer chooser is an autocomplete field.">
<b-checkbox name="rattail.customers.choice_uses_dropdown"
v-model="simpleSettings['rattail.customers.choice_uses_dropdown']"
native-value="true"
@input="settingsNeedSaved = true">
Show customer chooser as dropdown (select) element
Use dropdown (select element) for Customer chooser
</b-checkbox>
</b-field>

View file

@ -4,8 +4,8 @@
<%def name="object_helpers()">
${parent.object_helpers()}
% if show_profiles_helper and instance.people:
${view_profiles_helper(instance.people)}
% if show_profiles_helper and show_profiles_people:
${view_profiles_helper(show_profiles_people)}
% endif
</%def>
@ -20,7 +20,12 @@
${parent.modify_this_page_vars()}
<script type="text/javascript">
% if expose_shoppers:
${form.component_studly}Data.shoppers = ${json.dumps(shoppers_data)|n}
% endif
% if expose_people:
${form.component_studly}Data.peopleData = ${json.dumps(people_data)|n}
% endif
ThisPage.methods.detachPerson = function(url) {
## TODO: this should require POST, but we will add that once

View file

@ -24,6 +24,8 @@
Customer Views
"""
from collections import OrderedDict
import sqlalchemy as sa
import colander
@ -84,6 +86,7 @@ class CustomerView(MasterView):
'wholesale',
'active_in_pos',
'active_in_pos_sticky',
'shoppers',
'people',
'groups',
'members',
@ -108,6 +111,16 @@ class CustomerView(MasterView):
default=False)
return self._expose_active_in_pos
def should_expose_shoppers(self):
return self.rattail_config.getbool('rattail',
'customers.expose_shoppers',
default=True)
def should_expose_people(self):
return self.rattail_config.getbool('rattail',
'customers.expose_people',
default=True)
def configure_grid(self, g):
super(CustomerView, self).configure_grid(g)
model = self.model
@ -198,15 +211,17 @@ class CustomerView(MasterView):
raise HTTPNotFound
def configure_common_form(self, f):
super(CustomerView, self).configure_common_form(f)
def configure_form(self, f):
super(CustomerView, self).configure_form(f)
customer = f.model_instance
permission_prefix = self.get_permission_prefix()
# default_email
f.set_renderer('default_email', self.render_default_email)
if not self.creating and customer.emails:
f.set_default('default_email', customer.emails[0].address)
# default_phone
f.set_renderer('default_phone', self.render_default_phone)
if not self.creating and customer.phones:
f.set_default('default_phone', customer.phones[0].number)
@ -233,6 +248,7 @@ class CustomerView(MasterView):
f.set_default('address_state', addr.state)
f.set_default('address_zipcode', addr.zipcode)
# email_preference
f.set_enum('email_preference', self.enum.EMAIL_PREFERENCE)
preferences = list(self.enum.EMAIL_PREFERENCE.items())
preferences.insert(0, ('', "(no preference)"))
@ -245,9 +261,21 @@ class CustomerView(MasterView):
f.set_readonly('person')
f.set_renderer('person', self.form_render_person)
# shoppers
if self.should_expose_shoppers():
if self.viewing:
f.set_renderer('shoppers', self.render_shoppers)
else:
f.remove('shoppers')
else:
f.remove('shoppers')
# people
if self.viewing:
f.set_renderer('people', self.render_people_buefy)
if self.should_expose_people():
if self.viewing:
f.set_renderer('people', self.render_people_buefy)
else:
f.remove('people')
else:
f.remove('people')
@ -258,11 +286,7 @@ class CustomerView(MasterView):
f.set_renderer('groups', self.render_groups)
f.set_readonly('groups')
def configure_form(self, f):
super(CustomerView, self).configure_form(f)
customer = f.model_instance
permission_prefix = self.get_permission_prefix()
# active_in_pos*
if not self.get_expose_active_in_pos():
f.remove('active_in_pos',
'active_in_pos_sticky')
@ -275,32 +299,66 @@ class CustomerView(MasterView):
f.set_readonly('members')
def template_kwargs_view(self, **kwargs):
kwargs = super(CustomerView, self).template_kwargs_view(**kwargs)
kwargs = super().template_kwargs_view(**kwargs)
customer = kwargs['instance']
kwargs['expose_shoppers'] = self.should_expose_shoppers()
if kwargs['expose_shoppers']:
shoppers = []
for shopper in customer.shoppers:
person = shopper.person
active = None
if shopper.active is not None:
active = "Yes" if shopper.active else "No"
data = {
'uuid': shopper.uuid,
'shopper_number': shopper.shopper_number,
'first_name': person.first_name,
'last_name': person.last_name,
'full_name': person.display_name,
'phone': person.first_phone_number(),
'email': person.first_email_address(),
'active': active,
}
shoppers.append(data)
kwargs['shoppers_data'] = shoppers
kwargs['expose_people'] = self.should_expose_people()
if kwargs['expose_people']:
people = []
for person in customer.people:
data = {
'uuid': person.uuid,
'full_name': person.display_name,
'first_name': person.first_name,
'last_name': person.last_name,
'_action_url_view': self.request.route_url('people.view',
uuid=person.uuid),
}
if self.editable and self.request.has_perm('people.edit'):
data['_action_url_edit'] = self.request.route_url(
'people.edit',
uuid=person.uuid)
if self.people_detachable and self.has_perm('detach_person'):
data['_action_url_detach'] = self.request.route_url(
'customers.detach_person',
uuid=customer.uuid,
person_uuid=person.uuid)
people.append(data)
kwargs['people_data'] = people
kwargs['show_profiles_helper'] = self.show_profiles_helper
if kwargs['show_profiles_helper']:
people = OrderedDict()
customer = kwargs['instance']
people = []
for person in customer.people:
data = {
'uuid': person.uuid,
'full_name': person.display_name,
'first_name': person.first_name,
'last_name': person.last_name,
'_action_url_view': self.request.route_url('people.view',
uuid=person.uuid),
}
if self.editable and self.request.has_perm('people.edit'):
data['_action_url_edit'] = self.request.route_url(
'people.edit',
uuid=person.uuid)
if self.people_detachable and self.has_perm('detach_person'):
data['_action_url_detach'] = self.request.route_url(
'customers.detach_person',
uuid=customer.uuid,
person_uuid=person.uuid)
people.append(data)
kwargs['people_data'] = people
for shopper in customer.shoppers:
person = shopper.person
people.setdefault(person.uuid, person)
for person in customer.people:
people.setdefault(person.uuid, person)
kwargs['show_profiles_people'] = list(people.values())
return kwargs
@ -375,6 +433,35 @@ class CustomerView(MasterView):
main_actions=actions)
return HTML.literal(g.render_grid())
def render_shoppers(self, customer, field):
route_prefix = self.get_route_prefix()
permission_prefix = self.get_permission_prefix()
factory = self.get_grid_factory()
g = factory(
key='{}.people'.format(route_prefix),
data=[],
columns=[
'shopper_number',
'first_name',
'last_name',
'phone',
'email',
'active',
],
sortable=True,
sorters={'shopper_number': True,
'first_name': True,
'last_name': True,
'phone': True,
'email': True,
'active': True},
labels={'shopper_number': "Shopper #"},
)
return HTML.literal(
g.render_buefy_table_element(data_prop='shoppers'))
def render_people_buefy(self, customer, field):
route_prefix = self.get_route_prefix()
permission_prefix = self.get_permission_prefix()
@ -492,6 +579,14 @@ class CustomerView(MasterView):
{'section': 'rattail',
'option': 'customers.choice_uses_dropdown',
'type': bool},
{'section': 'rattail',
'option': 'customers.expose_shoppers',
'type': bool,
'default': True},
{'section': 'rattail',
'option': 'customers.expose_people',
'type': bool,
'default': True},
# POS
{'section': 'rattail',

View file

@ -4878,7 +4878,7 @@ class MasterView(View):
elif simple.get('type') is bool:
value = config.getbool(simple['section'],
simple['option'],
default=False)
default=simple.get('default', False))
else:
value = config.get(simple['section'],
simple['option'])

View file

@ -550,13 +550,16 @@ class PersonView(MasterView):
return context
def get_context_customers(self, person):
app = self.get_rattail_app()
clientele = app.get_clientele_handler()
customers = clientele.get_customers_for_account_holder(person)
key = self.get_customer_key_field()
data = []
for cp in person._customers:
customer = cp.customer
for customer in customers:
data.append({
'uuid': customer.uuid,
'ordinal': cp.ordinal,
'_key': getattr(customer, key),
'id': customer.id,
'number': customer.number,
@ -568,19 +571,19 @@ class PersonView(MasterView):
'addresses': [self.get_context_address(a)
for a in customer.addresses],
})
return data
def get_context_members(self, person):
app = self.get_rattail_app()
membership = app.get_membership_handler()
data = OrderedDict()
for member in person.members:
members = membership.get_members_for_account_holder(person)
for member in members:
data[member.uuid] = self.get_context_member(member)
for customer in person.customers:
for member in customer.members:
if member.uuid not in data:
data[member.uuid] = self.get_context_member(member)
return list(data.values())
def get_context_member(self, member):
@ -1115,6 +1118,40 @@ class PersonView(MasterView):
.filter(model.CustomerPerson.person_uuid == person.uuid)
versions.extend(query.all())
# nb. this is used in some queries below
FirstShopper = orm.aliased(model.CustomerShopper)
# CustomerShopper (from Customer perspective)
cls = continuum.version_class(model.CustomerShopper)
query = self.Session.query(cls)\
.join(model.Customer,
model.Customer.uuid == cls.customer_uuid)\
.filter(model.Customer.account_holder_uuid == person.uuid)
versions.extend(query.all())
# CustomerShopperHistory (from Customer perspective)
cls = continuum.version_class(model.CustomerShopperHistory)
query = self.Session.query(cls)\
.join(model.CustomerShopper,
model.CustomerShopper.uuid == cls.shopper_uuid)\
.join(model.Customer)\
.filter(model.Customer.account_holder_uuid == person.uuid)
versions.extend(query.all())
# CustomerShopper (from Shopper perspective)
cls = continuum.version_class(model.CustomerShopper)
query = self.Session.query(cls)\
.filter(cls.person_uuid == person.uuid)
versions.extend(query.all())
# CustomerShopperHistory (from Shopper perspective)
cls = continuum.version_class(model.CustomerShopperHistory)
query = self.Session.query(cls)\
.join(model.CustomerShopper,
model.CustomerShopper.uuid == cls.shopper_uuid)\
.filter(model.CustomerShopper.person_uuid == person.uuid)
versions.extend(query.all())
# PersonNote
cls = continuum.version_class(model.PersonNote)
query = self.Session.query(cls)\