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:
parent
afd5c3a5fd
commit
3fde80f991
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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)\
|
||||||
|
|
Loading…
Reference in a new issue