## $Id: browser.py 14208 2016-09-30 05:22:56Z henrik $ ## ## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann ## This program 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 2 of the License, or ## (at your option) any later version. ## ## This program 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 this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """UI components for customers and related components. """ import grok import os from urllib import urlencode from zope.event import notify from zope.i18n import translate from zope.catalog.interfaces import ICatalog from zope.component import queryUtility, getUtility, createObject from zope.security import checkPermission from hurry.workflow.interfaces import ( IWorkflowInfo, IWorkflowState, InvalidTransitionError) from waeup.ikoba.interfaces import MessageFactory as _ from waeup.ikoba.interfaces import ( IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils, IExtFileStore, IPasswordValidator, IUserAccount, STARTED, VERIFIED, REJECTED, EXPIRED, CREATED, REQUESTED, APPROVED, PROVISIONALLY, AWAITING) from waeup.ikoba.widgets.datewidget import ( FriendlyDateWidget, FriendlyDateDisplayWidget, FriendlyDatetimeDisplayWidget) from waeup.ikoba.browser.layout import ( IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage, NullValidator, jsaction, action, UtilityView) from waeup.ikoba.browser.pages import ContactAdminForm from waeup.ikoba.browser.breadcrumbs import Breadcrumb from waeup.ikoba.browser.interfaces import ICaptchaManager from waeup.ikoba.mandates.mandate import PasswordMandate from waeup.ikoba.payments.payment import ( get_payments_from_payer_id, get_payments_from_payable_id, find_payable_from_payable_id, get_payment_providers ) from waeup.ikoba.payments.interfaces import ( IPaymentGatewayServicesLister, IPaymentGatewayService, IPayer, IPayable, payment_states ) from waeup.ikoba.payments.catalog import search as search_payments from waeup.ikoba.widgets.hrefwidget import HREFDisplayWidget from waeup.ikoba.utils.helpers import ( get_current_principal, format_date) from waeup.ikoba.customers.interfaces import ( ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils, ICustomerDocument, ICustomerDocumentsContainer, ICustomerCreate, ICustomerPDFDocument, IContractsContainer, IContract, IContractSelectProduct, ) from waeup.ikoba.customers.catalog import search as search_customers from waeup.ikoba.customers.workflow import PAYMENT_TRANSITIONS grok.context(IIkobaObject) WARNING_CUST = _('You can not edit some data after final submission.' ' You really want to submit?') WARNING_DOC = _('You can not edit your document after final submission.' ' You really want to submit?') WARNING_CON = _('You can not edit your contract after final submission.' ' You really want to submit?') # Save function used for save methods in pages def msave(view, **data): changed_fields = view.applyData(view.context, **data) # Turn list of lists into single list if changed_fields: changed_fields = reduce(lambda x,y: x+y, changed_fields.values()) if 'product_object' in changed_fields and data['product_object'] is not None: view.context.last_product_id = data['product_object'].product_id notify(grok.ObjectModifiedEvent(view.context)) fields_string = ' + '.join(changed_fields) view.flash(_('Form has been saved.')) if fields_string: view.context.writeLogMessage( view, '%s - saved: %s' % (view.context.__name__, fields_string)) return def emit_lock_message(view): """Flash a lock message. """ view.flash(_('The requested form is locked (read-only).'), type="warning") view.redirect(view.url(view.context)) return def isCustomer(principal): return getattr(principal, 'user_type', None) == 'customer' class CustomersBreadcrumb(Breadcrumb): """A breadcrumb for the customers container. """ grok.context(ICustomersContainer) title = _('Customers') @property def target(self): user = get_current_principal() if getattr(user, 'user_type', None) == 'customer': return None return self.viewname class CustomerBreadcrumb(Breadcrumb): """A breadcrumb for the customer container. """ grok.context(ICustomer) def title(self): return self.context.display_fullname class CustomersContainerPage(IkobaPage): """The standard view for customer containers. """ grok.context(ICustomersContainer) grok.name('index') grok.require('waeup.viewCustomersContainer') grok.template('containerpage') label = _('Find customers') search_button = _('Find customer(s)') pnav = 4 def update(self, *args, **kw): form = self.request.form self.hitlist = [] if form.get('searchtype', None) == 'suspended': self.searchtype = form['searchtype'] self.searchterm = None elif 'searchterm' in form and form['searchterm']: self.searchterm = form['searchterm'] self.searchtype = form['searchtype'] elif 'old_searchterm' in form: self.searchterm = form['old_searchterm'] self.searchtype = form['old_searchtype'] else: if 'search' in form: self.flash(_('Empty search string'), type="warning") return self.hitlist = search_customers(query=self.searchterm, searchtype=self.searchtype, view=self) if not self.hitlist: self.flash(_('No customer found.'), type="warning") return class CustomersContainerManagePage(IkobaPage): """The manage page for customer containers. """ grok.context(ICustomersContainer) grok.name('manage') grok.require('waeup.manageCustomer') grok.template('containermanagepage') pnav = 4 label = _('Manage customer section') search_button = _('Find customer(s)') remove_button = _('Remove selected') def update(self, *args, **kw): form = self.request.form self.hitlist = [] if form.get('searchtype', None) == 'suspended': self.searchtype = form['searchtype'] self.searchterm = None elif 'searchterm' in form and form['searchterm']: self.searchterm = form['searchterm'] self.searchtype = form['searchtype'] elif 'old_searchterm' in form: self.searchterm = form['old_searchterm'] self.searchtype = form['old_searchtype'] else: if 'search' in form: self.flash(_('Empty search string'), type="warning") return if not 'entries' in form: self.hitlist = search_customers(query=self.searchterm, searchtype=self.searchtype, view=self) if not self.hitlist: self.flash(_('No customer found.'), type="warning") if 'remove' in form: self.flash(_('No item selected.'), type="warning") return entries = form['entries'] if isinstance(entries, basestring): entries = [entries] deleted = [] for entry in entries: if 'remove' in form: del self.context[entry] deleted.append(entry) self.hitlist = search_customers(query=self.searchterm, searchtype=self.searchtype, view=self) if len(deleted): self.flash(_('Successfully removed: ${a}', mapping={'a': ','.join(deleted)})) return class CustomerAddFormPage(IkobaAddFormPage): """Add-form to add a customer. """ grok.context(ICustomersContainer) grok.require('waeup.manageCustomer') grok.name('addcustomer') form_fields = grok.AutoFields(ICustomer).select( 'firstname', 'middlename', 'lastname', 'reg_number') label = _('Add customer') pnav = 4 @action(_('Create customer record'), style='primary') def addCustomer(self, **data): customer = createObject(u'waeup.Customer') self.applyData(customer, **data) self.context.addCustomer(customer) self.flash(_('Customer created.')) self.redirect(self.url(self.context[customer.customer_id], 'index')) return class LoginAsCustomerStep1(IkobaEditFormPage): """ View to temporarily set a customer password. """ grok.context(ICustomer) grok.name('loginasstep1') grok.require('waeup.loginAsCustomer') grok.template('loginasstep1') pnav = 4 def label(self): return _(u'Set temporary password for ${a}', mapping={'a': self.context.display_fullname}) @action('Set password now', style='primary') def setPassword(self, *args, **data): ikoba_utils = getUtility(IIkobaUtils) password = ikoba_utils.genPassword() self.context.setTempPassword(self.request.principal.id, password) self.context.writeLogMessage( self, 'temp_password generated: %s' % password) args = {'password': password} self.redirect(self.url(self.context) + '/loginasstep2?%s' % urlencode(args)) return class LoginAsCustomerStep2(IkobaPage): """ View to temporarily login as customer with a temporary password. """ grok.context(ICustomer) grok.name('loginasstep2') grok.require('waeup.Public') grok.template('loginasstep2') login_button = _('Login now') pnav = 4 def label(self): return _(u'Login as ${a}', mapping={'a': self.context.customer_id}) def update(self, SUBMIT=None, password=None): self.password = password if SUBMIT is not None: self.flash(_('You successfully logged in as customer.')) self.redirect(self.url(self.context)) return class CustomerBaseDisplayFormPage(IkobaDisplayFormPage): """ Page to display customer base data """ grok.context(ICustomer) grok.name('index') grok.require('waeup.viewCustomer') grok.template('basepage') pnav = 4 @property def form_fields(self): return grok.AutoFields( self.context.form_fields_interface).omit( 'password', 'suspended', 'suspended_comment') @property def label(self): if self.context.suspended: return _('${a}: Base Data (account deactivated)', mapping={'a': self.context.display_fullname}) return _('${a}: Base Data', mapping={'a': self.context.display_fullname}) @property def hasPassword(self): if self.context.password: return _('set') return _('unset') @property def is_requestable(self): if self.context.state in (REQUESTED, PROVISIONALLY, APPROVED): return False return True def update(self): # Fire transition if customer logs in for the first time usertype = getattr(self.request.principal, 'user_type', None) if usertype == 'customer' and \ IWorkflowState(self.context).getState() == CREATED: IWorkflowInfo(self.context).fireTransition('start') return class ContactCustomerForm(ContactAdminForm): grok.context(ICustomer) grok.name('contactcustomer') grok.require('waeup.viewCustomer') pnav = 4 form_fields = grok.AutoFields(IContactForm).select('subject', 'body') def update(self, subject=u'', body=u''): super(ContactCustomerForm, self).update() self.form_fields.get('subject').field.default = subject self.form_fields.get('body').field.default = body return def label(self): return _(u'Send message to ${a}', mapping={'a': self.context.display_fullname}) @action('Send message now', style='primary') def send(self, *args, **data): try: email = self.request.principal.email except AttributeError: email = self.config.email_admin usertype = getattr(self.request.principal, 'user_type', 'system').title() ikoba_utils = getUtility(IIkobaUtils) success = ikoba_utils.sendContactForm( self.request.principal.title, email, self.context.display_fullname, self.context.email, self.request.principal.id,usertype, self.config.name, data['body'], data['subject']) if success: self.flash(_('Your message has been sent.')) else: self.flash(_('An smtp server error occurred.'), type="danger") return class CustomerBaseManageFormPage(IkobaEditFormPage): """ View to manage customer base data """ grok.context(ICustomer) grok.name('manage_base') grok.require('waeup.manageCustomer') grok.template('basemanagepage') label = _('Manage base data') pnav = 4 deletion_warning = _('Are you sure?') @property def form_fields(self): return grok.AutoFields( self.context.form_fields_interface).omit( 'customer_id', 'suspended') def update(self): super(CustomerBaseManageFormPage, self).update() self.wf_info = IWorkflowInfo(self.context) return @action(_('Save'), style='primary') def save(self, **data): form = self.request.form password = form.get('password', None) password_ctl = form.get('control_password', None) if password: validator = getUtility(IPasswordValidator) errors = validator.validate_password(password, password_ctl) if errors: self.flash(' '.join(errors), type="danger") return changed_fields = self.applyData(self.context, **data) # Turn list of lists into single list if changed_fields: changed_fields = reduce(lambda x,y: x+y, changed_fields.values()) else: changed_fields = [] if password: # Now we know that the form has no errors and can set password IUserAccount(self.context).setPassword(password) changed_fields.append('password') fields_string = ' + '.join(changed_fields) self.flash(_('Form has been saved.')) if fields_string: self.context.writeLogMessage(self, 'saved: % s' % fields_string) return class CustomerTriggerTransitionFormPage(IkobaEditFormPage): """ View to trigger customer workflow transitions """ grok.context(ICustomer) grok.name('trigtrans') grok.require('waeup.triggerTransition') grok.template('trigtrans') label = _('Trigger registration transition') pnav = 4 def getTransitions(self): """Return a list of dicts of allowed transition ids and titles. Each list entry provides keys ``name`` and ``title`` for internal name and (human readable) title of a single transition. """ wf_info = IWorkflowInfo(self.context) allowed_transitions = [t for t in wf_info.getManualTransitions()] return [dict(name='', title=_('No transition'))] +[ dict(name=x, title=y) for x, y in allowed_transitions] @action(_('Apply'), style='primary') def apply(self, **data): form = self.request.form if 'transition' in form and form['transition']: transition_id = form['transition'] wf_info = IWorkflowInfo(self.context) wf_info.fireTransition(transition_id) self.flash(_("Transition '%s' executed." % transition_id)) self.redirect(self.url(self.context)) return class CustomerActivateView(UtilityView, grok.View): """ Activate customer account """ grok.context(ICustomer) grok.name('activate') grok.require('waeup.manageCustomer') def update(self): self.context.suspended = False self.context.writeLogMessage(self, 'account activated') history = IObjectHistory(self.context) history.addMessage('Customer account activated') self.flash(_('Customer account has been activated.')) self.redirect(self.url(self.context)) return def render(self): return class CustomerDeactivateView(UtilityView, grok.View): """ Deactivate customer account """ grok.context(ICustomer) grok.name('deactivate') grok.require('waeup.manageCustomer') def update(self): self.context.suspended = True self.context.writeLogMessage(self, 'account deactivated') history = IObjectHistory(self.context) history.addMessage('Customer account deactivated') self.flash(_('Customer account has been deactivated.')) self.redirect(self.url(self.context)) return def render(self): return class CustomerHistoryPage(IkobaPage): """ Page to display customer history """ grok.context(ICustomer) grok.name('history') grok.require('waeup.viewCustomer') grok.template('customerhistory') pnav = 4 @property def label(self): return _('${a}: History', mapping={'a':self.context.display_fullname}) class CustomerRequestPasswordPage(IkobaAddFormPage): """Captcha'd password request page for customers. """ grok.name('requestpw') grok.require('waeup.Anonymous') grok.template('requestpw') form_fields = grok.AutoFields(ICustomerRequestPW) label = _('Request password for first-time login') def update(self): # Handle captcha self.captcha = getUtility(ICaptchaManager).getCaptcha() self.captcha_result = self.captcha.verify(self.request) self.captcha_code = self.captcha.display(self.captcha_result.error_code) return def _redirect(self, email, password, customer_id): # Forward only email address to landing page in base package. self.redirect(self.url(self.context, 'requestpw_complete', data=dict(email=email))) return def _pw_used(self): # XXX: False if password has not been used. We need an extra # attribute which remembers if customer logged in. return True @action(_('Send login credentials to email address'), style='primary') def get_credentials(self, **data): if not self.captcha_result.is_valid: # Captcha will display error messages automatically. # No need to flash something. return number = data.get('number','') firstname = data.get('firstname','') cat = getUtility(ICatalog, name='customers_catalog') results = list( cat.searchResults(reg_number=(number, number))) if results: customer = results[0] if getattr(customer,'firstname',None) is None: self.flash(_('An error occurred.'), type="danger") return elif customer.firstname.lower() != firstname.lower(): # Don't tell the truth here. Anonymous must not # know that a record was found and only the firstname # verification failed. self.flash(_('No customer found.'), type="warning") return elif customer.password is not None and self._pw_used: self.flash(_('Your password has already been set and used. ' 'Please proceed to the login page.'), type="warning") return # Store email address but nothing else. customer.email = data['email'] notify(grok.ObjectModifiedEvent(customer)) else: # No record found, this is the truth. self.flash(_('No customer found.'), type="warning") return ikoba_utils = getUtility(IIkobaUtils) password = ikoba_utils.genPassword() mandate = PasswordMandate() mandate.params['password'] = password mandate.params['user'] = customer site = grok.getSite() site['mandates'].addMandate(mandate) # Send email with credentials args = {'mandate_id':mandate.mandate_id} mandate_url = self.url(site) + '/mandate?%s' % urlencode(args) url_info = u'Confirmation link: %s' % mandate_url msg = _('You have successfully requested a password for the') if ikoba_utils.sendCredentials(IUserAccount(customer), password, url_info, msg): email_sent = customer.email else: email_sent = None self._redirect(email=email_sent, password=password, customer_id=customer.customer_id) ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','') self.context.logger.info( '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent)) return class CustomerCreateAccountPage(IkobaAddFormPage): """Captcha'd account creation page for customers. """ grok.name('createaccount') grok.require('waeup.Anonymous') grok.template('createaccount') form_fields = grok.AutoFields(ICustomerCreate) label = _('Create customer account') def update(self): # Handle captcha self.captcha = getUtility(ICaptchaManager).getCaptcha() self.captcha_result = self.captcha.verify(self.request) self.captcha_code = self.captcha.display(self.captcha_result.error_code) return def _redirect(self, email, password, customer_id): # Forward only email address to landing page in base package. self.redirect(self.url(self.context, 'requestpw_complete', data=dict(email=email))) return @action(_('Send login credentials to email address'), style='primary') def create_account(self, **data): if not self.captcha_result.is_valid: # Captcha will display error messages automatically. # No need to flash something. return customer = createObject(u'waeup.Customer') customer.firstname = data.get('firstname','') customer.middlename = data.get('middlename','') customer.lastname = data.get('lastname','') customer.email = data.get('email','') self.context['customers'].addCustomer(customer) ikoba_utils = getUtility(IIkobaUtils) password = ikoba_utils.genPassword() mandate = PasswordMandate() mandate.params['password'] = password mandate.params['user'] = customer site = grok.getSite() site['mandates'].addMandate(mandate) # Send email with credentials args = {'mandate_id':mandate.mandate_id} mandate_url = self.url(site) + '/mandate?%s' % urlencode(args) url_info = u'Confirmation link: %s' % mandate_url msg = _('You have successfully created a customer account for the') if ikoba_utils.sendCredentials(IUserAccount(customer), password, url_info, msg): email_sent = customer.email else: email_sent = None self._redirect(email=email_sent, password=password, customer_id=customer.customer_id) ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','') self.context.logger.info( '%s - %s - %s' % (ob_class, customer.customer_id, email_sent)) return class CustomerRequestPasswordEmailSent(IkobaPage): """Landing page after successful password request. """ grok.name('requestpw_complete') grok.require('waeup.Public') grok.template('requestpwmailsent') label = _('Your request was successful.') def update(self, email=None, customer_id=None, password=None): self.email = email self.password = password self.customer_id = customer_id return # Pages for customers class CustomerFilesUploadPage(IkobaPage): """ View to upload files by customer We use this page only to upload a passport picture (portrait). """ grok.context(ICustomer) grok.name('upload_files') grok.require('waeup.handleCustomer') grok.template('filesuploadpage') label = _('Upload files') pnav = 4 deletion_warning = _('Are you sure?') def update(self, CANCEL=None): CUSTMANAGE_STATES = getUtility( ICustomersUtils).CUSTMANAGE_CUSTOMER_STATES if self.context.state not in CUSTMANAGE_STATES: emit_lock_message(self) return if CANCEL is not None: self.redirect(self.url(self.context)) return super(CustomerFilesUploadPage, self).update() return class CustomerBaseEditFormPage(IkobaEditFormPage): """ View to edit customer base data """ grok.context(ICustomer) grok.name('edit_base') grok.require('waeup.handleCustomer') pnav = 4 def is_requestable(self, action=None): if self.context.state == STARTED: return True return False @property def label(self): if self.is_requestable(): return _('Edit base data and request registration') return _('Edit base data') @property def form_fields(self): if not self.is_requestable(): return grok.AutoFields( self.context.form_fields_interface).select('email', 'phone') return grok.AutoFields(self.context.form_fields_interface).omit( 'suspended', 'suspended_comment', 'reg_number', 'customer_id') @action(_('Save'), style='primary') def save(self, **data): msave(self, **data) return def dataNotComplete(self): store = getUtility(IExtFileStore) error = '' if not store.getFileByContext(self.context, attr=u'passport.jpg'): error += _('Passport picture is missing.') if error: return error return @action(_('Save and request registration now'), warning=WARNING_CUST, condition=is_requestable, style='success') def finalsubmit(self, **data): msave(self, **data) if self.dataNotComplete(): self.flash(self.dataNotComplete(), type="warning") self.redirect(self.url(self.context, 'upload_files')) return IWorkflowInfo(self.context).fireTransition('request') self.flash(_('Registration form has been submitted.')) self.redirect(self.url(self.context)) return @action(_('Cancel'), validator=NullValidator) def cancel(self, **data): self.redirect(self.url(self.context)) class CustomerChangePasswordPage(IkobaEditFormPage): """ View to edit customer passords """ grok.context(ICustomer) grok.name('changepassword') grok.require('waeup.handleCustomer') grok.template('changepassword') label = _('Change password') pnav = 4 @action(_('Save'), style='primary') def save(self, **data): form = self.request.form password = form.get('change_password', None) password_ctl = form.get('change_password_repeat', None) if password: validator = getUtility(IPasswordValidator) errors = validator.validate_password(password, password_ctl) if not errors: IUserAccount(self.context).setPassword(password) # Unset temporary password self.context.temp_password = None self.context.writeLogMessage(self, 'saved: password') self.flash(_('Password changed.')) else: self.flash(' '.join(errors), type="warning") return @action(_('Cancel'), validator=NullValidator) def cancel(self, **data): self.redirect(self.url(self.context)) class CustomerBasePDFFormPage(IkobaDisplayFormPage): """ Page to display customer base data in pdf files. """ def __init__(self, context, request, omit_fields=()): self.omit_fields = omit_fields super(CustomerBasePDFFormPage, self).__init__(context, request) @property def form_fields(self): form_fields = grok.AutoFields(self.context.form_fields_interface) for field in self.omit_fields: form_fields = form_fields.omit(field) return form_fields # Pages for customer documents class DocumentsBreadcrumb(Breadcrumb): """A breadcrumb for the documents container. """ grok.context(ICustomerDocumentsContainer) title = _('Documents') class DocumentBreadcrumb(Breadcrumb): """A breadcrumb for the customer container. """ grok.context(ICustomerDocument) @property def title(self): return "%s" % self.context.document_id[:9] class DocumentsManageFormPage(IkobaEditFormPage): """ Page to manage the customer documents This manage form page is for both customers and customers officers. """ grok.context(ICustomerDocumentsContainer) grok.name('index') grok.require('waeup.viewCustomer') form_fields = grok.AutoFields(ICustomerDocumentsContainer) grok.template('documentsmanagepage') pnav = 4 @property def manage_documents_allowed(self): return checkPermission('waeup.editCustomerDocuments', self.context) def unremovable(self, document): usertype = getattr(self.request.principal, 'user_type', None) if not usertype: return False if not self.manage_documents_allowed: return True return (self.request.principal.user_type == 'customer' and \ document.state in (VERIFIED, REJECTED, EXPIRED)) @property def label(self): return _('${a}: Documents', mapping = {'a':self.context.__parent__.display_fullname}) @action(_('Add document'), validator=NullValidator, style='primary') def addDocument(self, **data): self.redirect(self.url(self.context, 'adddoc')) return @jsaction(_('Remove selected documents')) def delDocument(self, **data): form = self.request.form if 'val_id' in form: child_id = form['val_id'] else: self.flash(_('No document selected.'), type="warning") self.redirect(self.url(self.context)) return if not isinstance(child_id, list): child_id = [child_id] deleted = [] for id in child_id: # Customers are not allowed to remove used documents document = self.context.get(id, None) if document is not None and not self.unremovable(document): del self.context[id] deleted.append(id) if len(deleted): self.flash(_('Successfully removed: ${a}', mapping = {'a': ', '.join(deleted)})) self.context.writeLogMessage( self,'removed: %s' % ', '.join(deleted)) self.redirect(self.url(self.context)) return class DocumentAddFormPage(IkobaAddFormPage): """ Page to add a document This add form page is for both customers and customers officers. """ grok.context(ICustomerDocumentsContainer) grok.name('adddoc') grok.template('documentaddpage') grok.require('waeup.editCustomerDocuments') label = _('Add document') pnav = 4 form_fields = grok.AutoFields(ICustomerDocument).omit('document_id') @property def selectable_doctypes(self): doctypes = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT return sorted(doctypes.items()) @property def edit_documents_allowed(self): right_customer_state = self.context.customer.state in getUtility( ICustomersUtils).DOCMANAGE_CUSTOMER_STATES if isCustomer(self.request.principal) and not right_customer_state: return False return True def update(self): if not self.edit_documents_allowed: emit_lock_message(self) return return super(DocumentAddFormPage, self).update() @action(_('Add document'), style='primary') def createDocument(self, **data): form = self.request.form doctype = form.get('doctype', None) # Here we can create various instances of CustomerDocument derived # classes depending on the doctype parameter given in form. document = createObject('waeup.%s' % doctype) self.applyData(document, **data) self.context.addDocument(document) doctype = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT[doctype] self.flash(_('${a} added.', mapping = {'a': doctype})) self.context.writeLogMessage( self,'added: %s %s' % (doctype, document.document_id)) isCustomer = getattr( self.request.principal, 'user_type', None) == 'customer' if isCustomer: self.redirect(self.url(document, 'edit') + '#tab2') else: self.redirect(self.url(document, 'manage') + '#tab2') return @action(_('Cancel'), validator=NullValidator) def cancel(self, **data): self.redirect(self.url(self.context)) class DocumentDisplayFormPage(IkobaDisplayFormPage): """ Page to view a document """ grok.context(ICustomerDocument) grok.name('index') grok.require('waeup.viewCustomer') grok.template('documentpage') pnav = 4 label = None # We render the context title in the documentpage template @property def form_fields(self): return grok.AutoFields(self.context.form_fields_interface) class DocumentManageFormPage(IkobaEditFormPage): """ Page to edit a document """ grok.context(ICustomerDocument) grok.name('manage') grok.require('waeup.manageCustomer') grok.template('documenteditpage') pnav = 4 deletion_warning = _('Are you sure?') @property def form_fields(self): return grok.AutoFields( self.context.form_fields_interface).omit('document_id') def update(self): if not self.context.is_editable_by_manager: emit_lock_message(self) return return super(DocumentManageFormPage, self).update() @property def label(self): return self.context.title @action(_('Save'), style='primary') def save(self, **data): msave(self, **data) return class DocumentEditFormPage(DocumentManageFormPage): """ Page to edit a document """ grok.name('edit') grok.require('waeup.handleCustomer') def update(self): if not self.context.is_editable_by_customer: emit_lock_message(self) return return super(DocumentEditFormPage, self).update() @action(_('Save'), style='primary') def save(self, **data): msave(self, **data) return @action(_('Final Submit'), warning=WARNING_DOC, style='success') def finalsubmit(self, **data): msave(self, **data) IWorkflowInfo(self.context).fireTransition('submit') self.flash(_('Form has been submitted.')) self.redirect(self.url(self.context)) return class DocumentTriggerTransitionFormPage(IkobaEditFormPage): """ View to trigger customer document transitions """ grok.context(ICustomerDocument) grok.name('trigtrans') grok.require('waeup.triggerTransition') grok.template('trigtrans') label = _('Trigger document transition') pnav = 4 def update(self): return super(IkobaEditFormPage, self).update() def getTransitions(self): """Return a list of dicts of allowed transition ids and titles. Each list entry provides keys ``name`` and ``title`` for internal name and (human readable) title of a single transition. """ wf_info = IWorkflowInfo(self.context) allowed_transitions = [t for t in wf_info.getManualTransitions()] return [dict(name='', title=_('No transition'))] +[ dict(name=x, title=y) for x, y in allowed_transitions] @action(_('Apply'), style='primary') def apply(self, **data): form = self.request.form if 'transition' in form and form['transition']: transition_id = form['transition'] wf_info = IWorkflowInfo(self.context) try: wf_info.fireTransition(transition_id) self.flash(_("Transition '%s' executed." % transition_id)) except InvalidTransitionError, error: self.flash(error, type="warning") self.redirect(self.url(self.context)) return class PDFDocumentsOverviewSlip(UtilityView, grok.View): """Deliver an overview slip. """ grok.context(ICustomerDocumentsContainer) grok.name('documents_overview_slip.pdf') grok.require('waeup.viewCustomer') prefix = 'form' omit_fields = ('suspended', 'sex', 'suspended_comment',) form_fields = None @property def label(self): portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE return translate(_('Documents of'), 'waeup.ikoba', target_language=portal_language) \ + ' %s' % self.context.customer.display_fullname @property def tabletitle(self): portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE tabletitle = [] tabletitle.append(translate(_('Customer Documents'), 'waeup.ikoba', target_language=portal_language)) return tabletitle def render(self): portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE #Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language) Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language) Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language) State = translate(_('State'), 'waeup.ikoba', target_language=portal_language) LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language) tableheader = [] tabledata = [] for i in range(1,3): tabledata.append(sorted( [value for value in self.context.values()])) tableheader.append([ #(Id, 'document_id', 2), (Title, 'title', 6), (Type, 'translated_class_name', 6), (State, 'translated_state', 2), (LT, 'formatted_transition_date', 3), ]) customerview = CustomerBasePDFFormPage(self.context.customer, self.request, self.omit_fields) customers_utils = getUtility(ICustomersUtils) return customers_utils.renderPDF( self, 'overview_slip.pdf', self.context.customer, customerview, tableheader=tableheader, tabledata=tabledata, omit_fields=self.omit_fields) class PDFDocumentSlip(UtilityView, grok.View): """Deliver pdf file including metadata. """ grok.context(ICustomerDocument) grok.name('document_slip.pdf') grok.require('waeup.viewCustomer') prefix = 'form' omit_fields = ('suspended', 'sex', 'suspended_comment',) form_fields =() @property def label(self): return '%s of %s\nTitle: %s' % ( self.context.translated_class_name, self.context.customer.display_fullname, self.context.title) def render(self): customerview = CustomerBasePDFFormPage(self.context.customer, self.request, self.omit_fields) customers_utils = getUtility(ICustomersUtils) return customers_utils.renderPDF( self, 'document_slip.pdf', self.context.customer, customerview, omit_fields=self.omit_fields) class PDFMergeDocumentSlip(PDFDocumentSlip): """Deliver pdf file including metadata. """ grok.context(ICustomerPDFDocument) def render(self): customerview = CustomerBasePDFFormPage(self.context.customer, self.request, self.omit_fields) customers_utils = getUtility(ICustomersUtils) if self.context.state == VERIFIED: watermark_path = os.path.join( os.path.dirname(__file__), 'static', 'verified.pdf') else: watermark_path = os.path.join( os.path.dirname(__file__), 'static', 'unverified.pdf') watermark = open(watermark_path, 'rb') return customers_utils.renderPDF( self, 'pdfdocument_slip.pdf', self.context.customer, customerview, omit_fields=self.omit_fields, mergefiles=self.context.connected_files, watermark=watermark) # Pages for customer contracts class ContractsBreadcrumb(Breadcrumb): """A breadcrumb for the contracts container. """ grok.context(IContractsContainer) title = _('Contracts') class ContractBreadcrumb(Breadcrumb): """A breadcrumb for the customer container. """ grok.context(IContract) @property def title(self): return "%s" % self.context.contract_id[:9] class ContractsFormPage(IkobaEditFormPage): """ Page to display, edit or manage customer contracts This form page is for both customers and officers. """ grok.context(IContractsContainer) grok.name('index') grok.require('waeup.viewCustomer') form_fields = grok.AutoFields(IContractsContainer) grok.template('contractsmanagepage') pnav = 4 @property def edit_contracts_allowed(self): right_customer_state = self.context.customer.state in getUtility( ICustomersUtils).CONMANAGE_CUSTOMER_STATES if isCustomer(self.request.principal) and not right_customer_state: return False return checkPermission('waeup.editContracts', self.context) def remove_contract_allowed(self, contract): if isCustomer(self.request.principal): right_customer_state = self.context.customer.state in getUtility( ICustomersUtils).CONMANAGE_CUSTOMER_STATES if not right_customer_state: return False if contract.state in (APPROVED, AWAITING, REJECTED, EXPIRED): return False return checkPermission('waeup.editContracts', self.context) @property def label(self): return _('${a}: Contracts', mapping = {'a':self.context.__parent__.display_fullname}) @action(_('Add contract'), validator=NullValidator, style='primary') def addContract(self, **data): self.redirect(self.url(self.context, 'addcontract')) return @jsaction(_('Remove selected contracts')) def delContract(self, **data): form = self.request.form if 'val_id' in form: child_id = form['val_id'] else: self.flash(_('No contract selected.'), type="warning") self.redirect(self.url(self.context)) return if not isinstance(child_id, list): child_id = [child_id] deleted = [] for id in child_id: # Customers are not allowed to remove used contracts contract = self.context.get(id, None) if contract is not None and self.remove_contract_allowed(contract): del self.context[id] deleted.append(id) if len(deleted): self.flash(_('Successfully removed: ${a}', mapping = {'a': ', '.join(deleted)})) self.context.writeLogMessage( self,'removed: %s' % ', '.join(deleted)) self.redirect(self.url(self.context)) return class ContractAddFormPage(IkobaAddFormPage): """ Page to add an contract This page is for both customers and officers. """ grok.context(IContractsContainer) grok.name('addcontract') grok.template('contractaddpage') grok.require('waeup.editContracts') label = _('Add contract') pnav = 4 @property def edit_contracts_allowed(self): right_customer_state = self.context.customer.state in getUtility( ICustomersUtils).CONMANAGE_CUSTOMER_STATES if right_customer_state: return True return False def update(self): if not self.edit_contracts_allowed: emit_lock_message(self) return return super(ContractAddFormPage, self).update() @property def selectable_contypes(self): contypes = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT return sorted(contypes.items()) @action(_('Add contract'), style='primary') def createContract(self, **data): form = self.request.form contype = form.get('contype', None) # Here we can create various instances of Contract derived # classes depending on the contype parameter given in form. contract = createObject('waeup.%s' % contype) self.context.addContract(contract) contype = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT[contype] self.flash(_('${a} added.', mapping = {'a': contype})) self.context.writeLogMessage( self,'added: %s %s' % (contype, contract.contract_id)) self.redirect(self.url(contract, 'selectproduct')) return @action(_('Cancel'), validator=NullValidator) def cancel(self, **data): self.redirect(self.url(self.context)) class ContractSelectProductPage(IkobaAddFormPage): """ Page to select a contract product This page is for both customers and officers. """ grok.context(IContract) grok.name('selectproduct') grok.require('waeup.editContracts') label = _('Select product') pnav = 4 form_fields = grok.AutoFields(IContractSelectProduct) def update(self): if self.context.product_object is not None: emit_lock_message(self) return return super(ContractSelectProductPage, self).update() @action(_('Save and proceed'), style='primary') def save(self, **data): msave(self, **data) self.context.title = self.context.product_object.contract_autotitle self.context.tc_dict = self.context.product_object.tc_dict self.context.valid_from = self.context.product_object.valid_from self.context.valid_to = self.context.product_object.valid_to isCustomer = getattr( self.request.principal, 'user_type', None) == 'customer' if isCustomer: self.redirect(self.url(self.context, 'edit')) else: self.redirect(self.url(self.context, 'manage')) return class ContractDisplayFormPage(IkobaDisplayFormPage): """ Page to view a contract """ grok.context(IContract) grok.name('index') grok.require('waeup.viewCustomer') grok.template('contractpage') pnav = 4 label = None # We render the context title in the contractpage template @property def separators(self): return getUtility(ICustomersUtils).SEPARATORS_DICT @property def form_fields(self): form_fields = grok.AutoFields(self.context.form_fields_interface) for field in form_fields: if field.__name__.endswith('_object'): form_fields[field.__name__].custom_widget = HREFDisplayWidget return form_fields @property def terms_and_conditions(self): lang = self.request.cookies.get('ikoba.language') html = self.context.tc_dict.get(lang,'') if html =='': portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE html = self.context.tc_dict.get(portal_language,'') return html class ContractManageFormPage(IkobaEditFormPage): """ Page to manage a contract """ grok.context(IContract) grok.name('manage') grok.require('waeup.manageCustomer') grok.template('contracteditpage') pnav = 4 deletion_warning = _('Are you sure?') def update(self): if not self.context.is_editable: emit_lock_message(self) return return super(ContractManageFormPage, self).update() @property def separators(self): return getUtility(ICustomersUtils).SEPARATORS_DICT @property def terms_and_conditions(self): lang = self.request.cookies.get('ikoba.language') html = self.context.tc_dict.get(lang,'') if html =='': portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE html = self.context.tc_dict.get(portal_language,'') return html @property def form_fields(self): return grok.AutoFields(self.context.form_fields_interface).omit( 'contract_id', 'product_object') @property def label(self): return self.context.title @action(_('Save'), style='primary') def save(self, **data): msave(self, **data) return class ContractOfficialUsePage(IkobaEditFormPage): """ Page to manage a official use data of contract """ grok.context(IContract) grok.name('official_use') grok.require('waeup.manageCustomer') grok.template('contracteditpage') pnav = 4 deletion_warning = _('Are you sure?') terms_and_conditions = None @property def form_fields(self): return grok.AutoFields(self.context.ou_form_fields_interface) @property def label(self): return self.context.title @action(_('Save'), style='primary') def save(self, **data): msave(self, **data) return class ContractEditFormPage(ContractManageFormPage): """ Page to edit a contract by customer only """ grok.name('edit') grok.require('waeup.handleCustomer') def submission_allowed(self, action=None): if self.context.state == CREATED and not self.context.fee_based: return True return False def payment_expected(self, action=None): if self.context.state == CREATED and self.context.fee_based: return True return False @property def form_fields(self): return grok.AutoFields(self.context.edit_form_fields_interface).omit( 'contract_id', 'product_object') @action(_('Save'), style='primary') def save(self, **data): msave(self, **data) return @action(_('Apply now (final submission)'), warning=WARNING_CON, condition=submission_allowed, style='success') def submit(self, **data): if self.terms_and_conditions and not self.request.form.get( 'confirm_tc', False): self.flash(_('Please read the terms and conditions and ' 'confirm your acceptance of these by ticking ' 'the confirmation box.'), type="danger") return msave(self, **data) IWorkflowInfo(self.context).fireTransition('submit') self.flash(_('Application form has been submitted.')) self.redirect(self.url(self.context)) return @action(_('Proceed to checkout'), condition=payment_expected, style='primary') def proceed_checkout(self, **data): if self.terms_and_conditions and not self.request.form.get( 'confirm_tc', False): self.flash(_('Please read the terms and conditions and ' 'confirm your acceptance of these by ticking ' 'the confirmation box.'), type="danger") return msave(self, **data) self.redirect(self.url(self.context, 'select_payment_method')) return class ContractSelectPaymentMethodPage(IkobaEditFormPage): """ Page to to select payment method """ grok.context(IContract) grok.name('select_payment_method') grok.require('waeup.handleCustomer') grok.template('selectpaymentmethodpage') pnav = 4 label = _('Select payment method') @property def payment_gateways(self): """Get an iter over registered and enabled gateway service providers. We provide tuples ``(value, description)`` for each supported payment gateway. """ lister = getUtility(IPaymentGatewayServicesLister) for name, service in lister().items(): yield {'name': name, 'title': service.title} def update(self, CANCEL=None, gw=None): if self.context.state != CREATED or not self.context.fee_based: emit_lock_message(self) return self.gw = gw super(ContractSelectPaymentMethodPage, self).update() return @action(_('Select payment method (final submission)'), style='success') def confirm(self, **data): if self.gw is None: self.flash(_('Please pick a payment method.'), type='warning') else: service = queryUtility(IPaymentGatewayService, name=self.gw) if service is None: self.flash(_('Invalid payment gateway.'), type='danger') return payer = IPayer(self.context) payable = IPayable(self.context) payment = service.create_payment(payer, payable) service.store(payment) payment, view_name = service.next_step(payment.payment_id) url = self.url(payment, view_name) self.redirect(url) return return @action(_('Cancel')) def cancel(self, **data): self.redirect(self.url(self.context, 'edit')) return class ContractTriggerTransitionFormPage(IkobaEditFormPage): """ View to trigger customer contract transitions """ grok.context(IContract) grok.name('trigtrans') grok.require('waeup.triggerTransition') grok.template('trigtrans') label = _('Trigger contract transition') pnav = 4 def update(self): return super(IkobaEditFormPage, self).update() def getTransitions(self): """Return a list of dicts of allowed transition ids and titles. Each list entry provides keys ``name`` and ``title`` for internal name and (human readable) title of a single transition. """ wf_info = IWorkflowInfo(self.context) allowed_transitions = [t for t in wf_info.getManualTransitions()] # Skip payment transitions if total amount is zero if not self.context.fee_based: allowed_transitions = [t for t in allowed_transitions if not t[0] in PAYMENT_TRANSITIONS] return [dict(name='', title=_('No transition'))] +[ dict(name=x, title=y) for x, y in allowed_transitions] @action(_('Apply'), style='primary') def apply(self, **data): form = self.request.form if 'transition' in form and form['transition']: transition_id = form['transition'] wf_info = IWorkflowInfo(self.context) try: wf_info.fireTransition(transition_id) self.flash(_("Transition '%s' executed." % transition_id)) except InvalidTransitionError, error: self.flash(error, type="warning") self.redirect(self.url(self.context)) return class PDFContractsOverviewSlip(UtilityView, grok.View): """Deliver an overview slip. """ grok.context(IContractsContainer) grok.name('contracts_overview_slip.pdf') grok.require('waeup.viewCustomer') prefix = 'form' omit_fields = ('suspended', 'sex', 'suspended_comment',) form_fields = None @property def label(self): portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE return translate(_('Contracts of'), 'waeup.ikoba', target_language=portal_language) \ + ' %s' % self.context.customer.display_fullname @property def tabletitle(self): portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE tabletitle = [] tabletitle.append(translate(_('Customer Contracts'), 'waeup.ikoba', target_language=portal_language)) return tabletitle def render(self): portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE #Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language) Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language) Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language) State = translate(_('State'), 'waeup.ikoba', target_language=portal_language) LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language) tableheader = [] tabledata = [] for i in range(1,3): tabledata.append(sorted( [value for value in self.context.values()])) tableheader.append([ #(Id, 'contract_id', 2), (Title, 'title', 6), (Type, 'translated_class_name', 6), (State, 'translated_state', 2), (LT, 'formatted_transition_date', 3), ]) customerview = CustomerBasePDFFormPage(self.context.customer, self.request, self.omit_fields) customers_utils = getUtility(ICustomersUtils) return customers_utils.renderPDF( self, 'overview_slip.pdf', self.context.customer, customerview, tableheader=tableheader, tabledata=tabledata, omit_fields=self.omit_fields) class PDFContractSlip(UtilityView, grok.View): """Deliver pdf file including metadata. """ grok.context(IContract) grok.name('contract_slip.pdf') grok.require('waeup.viewCustomer') prefix = 'form' omit_fields = ('suspended', 'sex', 'suspended_comment',) @property def form_fields(self): return grok.AutoFields(self.context.form_fields_interface) @property def terms_and_conditions(self): lang = self.request.cookies.get('ikoba.language') html = self.context.tc_dict.get(lang,'') if html =='': portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE html = self.context.tc_dict.get(portal_language,'') return html @property def validity_period(self): if self.context.valid_from or self.context.valid_to: return "%s - %s" % (format_date(self.context.valid_from, self.request), format_date(self.context.valid_to, self.request)) return @property def label(self): return self.context.title # XXX: not used in waeup.ikoba and thus not tested def _signatures(self): return ([_('Customer Signature')], [_('Company Officer Signature')] ) def _sigsInFooter(self): return (_('Date, Customer Signature'), _('Date, Company Officer Signature'), ) def render(self): customerview = CustomerBasePDFFormPage(self.context.customer, self.request, self.omit_fields) customers_utils = getUtility(ICustomersUtils) return customers_utils.renderPDF( self, 'contract_slip.pdf', self.context.customer, customerview, signatures=self._signatures(), sigs_in_footer=self._sigsInFooter(), omit_fields=self.omit_fields) class PaymentsPage(IkobaPage): """ Page to display all payments made by customer. """ grok.context(ICustomer) grok.name('payments') grok.require('waeup.manageCustomer') grok.template('paymentspage') pnav = 4 @property def label(self): return _('${a}: Payments', mapping={'a':self.context.display_fullname}) @property def payment_states(self): return payment_states @property def gateway_services(self): return get_payment_providers() def payments(self): return search_payments( query=self.context.customer_id, searchtype='payer_id') class PDFContractReceiptSlip(UtilityView, grok.View): """Deliver pdf file including metadata. """ grok.context(IContract) grok.name('contract_payment_receipt.pdf') grok.require('waeup.viewCustomer') prefix = 'form' omit_fields = ('suspended', 'sex', 'suspended_comment',) @property def form_fields(self): return grok.AutoFields(self.context.form_fields_interface) @property def label(self): return 'Payment Receipt: %s' % ( self.context.title) @property def payment_tuples(self): payment_tuples = [] for payment in get_payments_from_payable_id(self.context.contract_id): form_fields = grok.AutoFields(payment.form_fields_interface).omit( 'payment_items') form_fields[ 'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le') form_fields[ 'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le') payment_tuples.append((payment, form_fields)) return payment_tuples def render(self): customerview = CustomerBasePDFFormPage(self.context.customer, self.request, self.omit_fields) customers_utils = getUtility(ICustomersUtils) return customers_utils.renderPDF( self, 'contract_payment_receipt.pdf', self.context.customer, customerview, omit_fields=self.omit_fields, show_history=False)