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
					
				
					 5 changed files with 201 additions and 46 deletions
				
			
		|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar