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>
<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" <b-checkbox name="rattail.customers.choice_uses_dropdown"
v-model="simpleSettings['rattail.customers.choice_uses_dropdown']" v-model="simpleSettings['rattail.customers.choice_uses_dropdown']"
native-value="true" native-value="true"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true">
Show customer chooser as dropdown (select) element Use dropdown (select element) for Customer chooser
</b-checkbox> </b-checkbox>
</b-field> </b-field>

View file

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

View file

@ -24,6 +24,8 @@
Customer Views Customer Views
""" """
from collections import OrderedDict
import sqlalchemy as sa import sqlalchemy as sa
import colander import colander
@ -84,6 +86,7 @@ class CustomerView(MasterView):
'wholesale', 'wholesale',
'active_in_pos', 'active_in_pos',
'active_in_pos_sticky', 'active_in_pos_sticky',
'shoppers',
'people', 'people',
'groups', 'groups',
'members', 'members',
@ -108,6 +111,16 @@ class CustomerView(MasterView):
default=False) default=False)
return self._expose_active_in_pos 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): def configure_grid(self, g):
super(CustomerView, self).configure_grid(g) super(CustomerView, self).configure_grid(g)
model = self.model model = self.model
@ -198,15 +211,17 @@ class CustomerView(MasterView):
raise HTTPNotFound raise HTTPNotFound
def configure_common_form(self, f): def configure_form(self, f):
super(CustomerView, self).configure_common_form(f) super(CustomerView, self).configure_form(f)
customer = f.model_instance customer = f.model_instance
permission_prefix = self.get_permission_prefix() permission_prefix = self.get_permission_prefix()
# default_email
f.set_renderer('default_email', self.render_default_email) f.set_renderer('default_email', self.render_default_email)
if not self.creating and customer.emails: if not self.creating and customer.emails:
f.set_default('default_email', customer.emails[0].address) f.set_default('default_email', customer.emails[0].address)
# default_phone
f.set_renderer('default_phone', self.render_default_phone) f.set_renderer('default_phone', self.render_default_phone)
if not self.creating and customer.phones: if not self.creating and customer.phones:
f.set_default('default_phone', customer.phones[0].number) 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_state', addr.state)
f.set_default('address_zipcode', addr.zipcode) f.set_default('address_zipcode', addr.zipcode)
# email_preference
f.set_enum('email_preference', self.enum.EMAIL_PREFERENCE) f.set_enum('email_preference', self.enum.EMAIL_PREFERENCE)
preferences = list(self.enum.EMAIL_PREFERENCE.items()) preferences = list(self.enum.EMAIL_PREFERENCE.items())
preferences.insert(0, ('', "(no preference)")) preferences.insert(0, ('', "(no preference)"))
@ -245,9 +261,21 @@ class CustomerView(MasterView):
f.set_readonly('person') f.set_readonly('person')
f.set_renderer('person', self.form_render_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 # people
if self.viewing: if self.should_expose_people():
f.set_renderer('people', self.render_people_buefy) if self.viewing:
f.set_renderer('people', self.render_people_buefy)
else:
f.remove('people')
else: else:
f.remove('people') f.remove('people')
@ -258,11 +286,7 @@ class CustomerView(MasterView):
f.set_renderer('groups', self.render_groups) f.set_renderer('groups', self.render_groups)
f.set_readonly('groups') f.set_readonly('groups')
def configure_form(self, f): # active_in_pos*
super(CustomerView, self).configure_form(f)
customer = f.model_instance
permission_prefix = self.get_permission_prefix()
if not self.get_expose_active_in_pos(): if not self.get_expose_active_in_pos():
f.remove('active_in_pos', f.remove('active_in_pos',
'active_in_pos_sticky') 'active_in_pos_sticky')
@ -275,32 +299,66 @@ class CustomerView(MasterView):
f.set_readonly('members') f.set_readonly('members')
def template_kwargs_view(self, **kwargs): 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 kwargs['show_profiles_helper'] = self.show_profiles_helper
if kwargs['show_profiles_helper']:
people = OrderedDict()
customer = kwargs['instance'] for shopper in customer.shoppers:
people = [] person = shopper.person
for person in customer.people: people.setdefault(person.uuid, person)
data = {
'uuid': person.uuid, for person in customer.people:
'full_name': person.display_name, people.setdefault(person.uuid, person)
'first_name': person.first_name,
'last_name': person.last_name, kwargs['show_profiles_people'] = list(people.values())
'_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
return kwargs return kwargs
@ -375,6 +433,35 @@ class CustomerView(MasterView):
main_actions=actions) main_actions=actions)
return HTML.literal(g.render_grid()) 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): def render_people_buefy(self, customer, field):
route_prefix = self.get_route_prefix() route_prefix = self.get_route_prefix()
permission_prefix = self.get_permission_prefix() permission_prefix = self.get_permission_prefix()
@ -492,6 +579,14 @@ class CustomerView(MasterView):
{'section': 'rattail', {'section': 'rattail',
'option': 'customers.choice_uses_dropdown', 'option': 'customers.choice_uses_dropdown',
'type': bool}, 'type': bool},
{'section': 'rattail',
'option': 'customers.expose_shoppers',
'type': bool,
'default': True},
{'section': 'rattail',
'option': 'customers.expose_people',
'type': bool,
'default': True},
# POS # POS
{'section': 'rattail', {'section': 'rattail',

View file

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

View file

@ -550,13 +550,16 @@ class PersonView(MasterView):
return context return context
def get_context_customers(self, person): 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() key = self.get_customer_key_field()
data = [] data = []
for cp in person._customers:
customer = cp.customer for customer in customers:
data.append({ data.append({
'uuid': customer.uuid, 'uuid': customer.uuid,
'ordinal': cp.ordinal,
'_key': getattr(customer, key), '_key': getattr(customer, key),
'id': customer.id, 'id': customer.id,
'number': customer.number, 'number': customer.number,
@ -568,19 +571,19 @@ class PersonView(MasterView):
'addresses': [self.get_context_address(a) 'addresses': [self.get_context_address(a)
for a in customer.addresses], for a in customer.addresses],
}) })
return data return data
def get_context_members(self, person): def get_context_members(self, person):
app = self.get_rattail_app()
membership = app.get_membership_handler()
data = OrderedDict() 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) 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()) return list(data.values())
def get_context_member(self, member): def get_context_member(self, member):
@ -1115,6 +1118,40 @@ class PersonView(MasterView):
.filter(model.CustomerPerson.person_uuid == person.uuid) .filter(model.CustomerPerson.person_uuid == person.uuid)
versions.extend(query.all()) 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 # PersonNote
cls = continuum.version_class(model.PersonNote) cls = continuum.version_class(model.PersonNote)
query = self.Session.query(cls)\ query = self.Session.query(cls)\