source: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py @ 12131

Last change on this file since 12131 was 12119, checked in by Henrik Bettermann, 10 years ago

Implement HREFDisplayWidget which renders a persistent object title inside an anchor element
referring to the object.

  • Property svn:keywords set to Id
File size: 46.2 KB
RevLine 
[12015]1## $Id: browser.py 12119 2014-12-02 14:38:31Z henrik $
[11958]2##
3## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for customers and related components.
19"""
20
[11967]21import sys
[11958]22import grok
[11967]23import pytz
24from urllib import urlencode
25from datetime import datetime
26from zope.event import notify
27from zope.i18n import translate
28from zope.catalog.interfaces import ICatalog
29from zope.component import queryUtility, getUtility, createObject
30from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
31from zope.formlib.textwidgets import BytesDisplayWidget
32from zope.security import checkPermission
33from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[11958]34from waeup.ikoba.interfaces import MessageFactory as _
[11971]35from waeup.ikoba.interfaces import (
36    IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils,
[12089]37    IPasswordValidator, IUserAccount,
[12091]38    VERIFIED, REJECTED, EXPIRED, APPROVED)
[11958]39from waeup.ikoba.browser.layout import (
40    IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
[11967]41    IkobaForm, NullValidator, jsaction, action, UtilityView)
[12053]42from waeup.ikoba.widgets.datewidget import (
43    FriendlyDateWidget, FriendlyDateDisplayWidget,
44    FriendlyDatetimeDisplayWidget)
[11967]45from waeup.ikoba.browser.pages import ContactAdminForm
[11958]46from waeup.ikoba.browser.breadcrumbs import Breadcrumb
[11971]47from waeup.ikoba.browser.interfaces import ICaptchaManager
[11977]48from waeup.ikoba.mandates.mandate import PasswordMandate
[12119]49from waeup.ikoba.widgets.hrefwidget import HREFDisplayWidget
[11958]50from waeup.ikoba.utils.helpers import get_current_principal, to_timezone, now
51from waeup.ikoba.customers.interfaces import (
[12015]52    ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils,
[12062]53    ICustomerDocument, ICustomerDocumentsContainer, ICustomerCreate,
[12103]54    ICustomerPDFDocument, IContractsContainer, IContract, IContractEdit,
55    ISampleContract,
[11958]56    )
57from waeup.ikoba.customers.catalog import search
58
[11967]59grok.context(IIkobaObject)
60
[12034]61WARNING = _('You can not edit your document after final submission.'
62            ' You really want to submit?')
[11985]63
[12034]64
[11986]65# Save function used for save methods in pages
66def msave(view, **data):
67    changed_fields = view.applyData(view.context, **data)
68    # Turn list of lists into single list
69    if changed_fields:
70        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[12119]71    if 'product_object' in changed_fields and data['product_object'] is not None:
72        view.context.last_product_id = data['product_object'].product_id
[12096]73        notify(grok.ObjectModifiedEvent(view.context))
[11986]74    fields_string = ' + '.join(changed_fields)
75    view.flash(_('Form has been saved.'))
76    if fields_string:
[12091]77        view.context.writeLogMessage(
[12096]78            view, '%s - saved: %s' % (view.context.__name__, fields_string))
[11986]79    return
80
81
82def emit_lock_message(view):
83    """Flash a lock message.
84    """
85    view.flash(_('The requested form is locked (read-only).'), type="warning")
86    view.redirect(view.url(view.context))
87    return
88
89
[11958]90class CustomersBreadcrumb(Breadcrumb):
91    """A breadcrumb for the customers container.
92    """
93    grok.context(ICustomersContainer)
94    title = _('Customers')
95
96    @property
97    def target(self):
98        user = get_current_principal()
99        if getattr(user, 'user_type', None) == 'customer':
100            return None
101        return self.viewname
102
[11967]103
104class CustomerBreadcrumb(Breadcrumb):
105    """A breadcrumb for the customer container.
106    """
107    grok.context(ICustomer)
108
109    def title(self):
110        return self.context.display_fullname
111
[11985]112
[11958]113class CustomersContainerPage(IkobaPage):
114    """The standard view for customer containers.
115    """
116    grok.context(ICustomersContainer)
117    grok.name('index')
118    grok.require('waeup.viewCustomersContainer')
119    grok.template('containerpage')
120    label = _('Find customers')
121    search_button = _('Find customer(s)')
122    pnav = 4
123
124    def update(self, *args, **kw):
125        form = self.request.form
126        self.hitlist = []
127        if form.get('searchtype', None) == 'suspended':
128            self.searchtype = form['searchtype']
129            self.searchterm = None
130        elif 'searchterm' in form and form['searchterm']:
131            self.searchterm = form['searchterm']
132            self.searchtype = form['searchtype']
133        elif 'old_searchterm' in form:
134            self.searchterm = form['old_searchterm']
135            self.searchtype = form['old_searchtype']
136        else:
137            if 'search' in form:
138                self.flash(_('Empty search string'), type="warning")
139            return
140        if self.searchtype == 'current_session':
141            try:
142                self.searchterm = int(self.searchterm)
143            except ValueError:
144                self.flash(_('Only year dates allowed (e.g. 2011).'),
145                           type="danger")
146                return
147        self.hitlist = search(query=self.searchterm,
148            searchtype=self.searchtype, view=self)
149        if not self.hitlist:
150            self.flash(_('No customer found.'), type="warning")
151        return
152
[11985]153
[11958]154class CustomersContainerManagePage(IkobaPage):
155    """The manage page for customer containers.
156    """
157    grok.context(ICustomersContainer)
158    grok.name('manage')
159    grok.require('waeup.manageCustomer')
160    grok.template('containermanagepage')
161    pnav = 4
162    label = _('Manage customer section')
163    search_button = _('Find customer(s)')
164    remove_button = _('Remove selected')
165
166    def update(self, *args, **kw):
167        form = self.request.form
168        self.hitlist = []
169        if form.get('searchtype', None) == 'suspended':
170            self.searchtype = form['searchtype']
171            self.searchterm = None
172        elif 'searchterm' in form and form['searchterm']:
173            self.searchterm = form['searchterm']
174            self.searchtype = form['searchtype']
175        elif 'old_searchterm' in form:
176            self.searchterm = form['old_searchterm']
177            self.searchtype = form['old_searchtype']
178        else:
179            if 'search' in form:
180                self.flash(_('Empty search string'), type="warning")
181            return
182        if self.searchtype == 'current_session':
183            try:
184                self.searchterm = int(self.searchterm)
185            except ValueError:
186                self.flash(_('Only year dates allowed (e.g. 2011).'),
187                           type="danger")
188                return
189        if not 'entries' in form:
190            self.hitlist = search(query=self.searchterm,
191                searchtype=self.searchtype, view=self)
192            if not self.hitlist:
193                self.flash(_('No customer found.'), type="warning")
194            if 'remove' in form:
195                self.flash(_('No item selected.'), type="warning")
196            return
197        entries = form['entries']
198        if isinstance(entries, basestring):
199            entries = [entries]
200        deleted = []
201        for entry in entries:
202            if 'remove' in form:
203                del self.context[entry]
204                deleted.append(entry)
205        self.hitlist = search(query=self.searchterm,
206            searchtype=self.searchtype, view=self)
207        if len(deleted):
208            self.flash(_('Successfully removed: ${a}',
[11985]209                mapping={'a': ','.join(deleted)}))
[11967]210        return
211
[11985]212
[11967]213class CustomerAddFormPage(IkobaAddFormPage):
214    """Add-form to add a customer.
215    """
216    grok.context(ICustomersContainer)
217    grok.require('waeup.manageCustomer')
218    grok.name('addcustomer')
219    form_fields = grok.AutoFields(ICustomer).select(
220        'firstname', 'middlename', 'lastname', 'reg_number')
221    label = _('Add customer')
222    pnav = 4
223
224    @action(_('Create customer record'), style='primary')
225    def addCustomer(self, **data):
226        customer = createObject(u'waeup.Customer')
227        self.applyData(customer, **data)
228        self.context.addCustomer(customer)
229        self.flash(_('Customer record created.'))
230        self.redirect(self.url(self.context[customer.customer_id], 'index'))
231        return
232
[11985]233
[11967]234class LoginAsCustomerStep1(IkobaEditFormPage):
235    """ View to temporarily set a customer password.
236    """
237    grok.context(ICustomer)
238    grok.name('loginasstep1')
239    grok.require('waeup.loginAsCustomer')
240    grok.template('loginasstep1')
241    pnav = 4
242
243    def label(self):
244        return _(u'Set temporary password for ${a}',
[11985]245            mapping={'a': self.context.display_fullname})
[11967]246
247    @action('Set password now', style='primary')
248    def setPassword(self, *args, **data):
[11979]249        ikoba_utils = getUtility(IIkobaUtils)
250        password = ikoba_utils.genPassword()
[11967]251        self.context.setTempPassword(self.request.principal.id, password)
252        self.context.writeLogMessage(
253            self, 'temp_password generated: %s' % password)
[11985]254        args = {'password': password}
[11967]255        self.redirect(self.url(self.context) +
256            '/loginasstep2?%s' % urlencode(args))
257        return
258
[11985]259
[11967]260class LoginAsCustomerStep2(IkobaPage):
261    """ View to temporarily login as customer with a temporary password.
262    """
263    grok.context(ICustomer)
264    grok.name('loginasstep2')
265    grok.require('waeup.Public')
266    grok.template('loginasstep2')
267    login_button = _('Login now')
268    pnav = 4
269
270    def label(self):
271        return _(u'Login as ${a}',
[11985]272            mapping={'a': self.context.customer_id})
[11967]273
274    def update(self, SUBMIT=None, password=None):
275        self.password = password
276        if SUBMIT is not None:
277            self.flash(_('You successfully logged in as customer.'))
278            self.redirect(self.url(self.context))
279        return
280
[11985]281
[11967]282class CustomerBaseDisplayFormPage(IkobaDisplayFormPage):
283    """ Page to display customer base data
284    """
285    grok.context(ICustomer)
286    grok.name('index')
287    grok.require('waeup.viewCustomer')
288    grok.template('basepage')
289    form_fields = grok.AutoFields(ICustomer).omit(
290        'password', 'suspended', 'suspended_comment')
291    pnav = 4
292
293    @property
294    def label(self):
295        if self.context.suspended:
296            return _('${a}: Base Data (account deactivated)',
[11985]297                mapping={'a': self.context.display_fullname})
[11967]298        return  _('${a}: Base Data',
[11985]299            mapping={'a': self.context.display_fullname})
[11967]300
301    @property
302    def hasPassword(self):
303        if self.context.password:
304            return _('set')
305        return _('unset')
306
[11985]307
[11967]308class ContactCustomerForm(ContactAdminForm):
309    grok.context(ICustomer)
310    grok.name('contactcustomer')
311    grok.require('waeup.viewCustomer')
312    pnav = 4
313    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
314
315    def update(self, subject=u'', body=u''):
316        super(ContactCustomerForm, self).update()
317        self.form_fields.get('subject').field.default = subject
318        self.form_fields.get('body').field.default = body
319        return
320
321    def label(self):
322        return _(u'Send message to ${a}',
[11985]323            mapping={'a': self.context.display_fullname})
[11967]324
325    @action('Send message now', style='primary')
326    def send(self, *args, **data):
327        try:
328            email = self.request.principal.email
329        except AttributeError:
330            email = self.config.email_admin
331        usertype = getattr(self.request.principal,
332                           'user_type', 'system').title()
[11979]333        ikoba_utils = getUtility(IIkobaUtils)
334        success = ikoba_utils.sendContactForm(
[11985]335                self.request.principal.title, email,
336                self.context.display_fullname, self.context.email,
[11967]337                self.request.principal.id,usertype,
338                self.config.name,
[11985]339                data['body'], data['subject'])
[11967]340        if success:
341            self.flash(_('Your message has been sent.'))
342        else:
343            self.flash(_('An smtp server error occurred.'), type="danger")
344        return
345
[11985]346
[11967]347class CustomerBaseManageFormPage(IkobaEditFormPage):
348    """ View to manage customer base data
349    """
350    grok.context(ICustomer)
351    grok.name('manage_base')
352    grok.require('waeup.manageCustomer')
353    form_fields = grok.AutoFields(ICustomer).omit(
354        'customer_id', 'adm_code', 'suspended')
355    grok.template('basemanagepage')
356    label = _('Manage base data')
357    pnav = 4
358
359    def update(self):
360        super(CustomerBaseManageFormPage, self).update()
361        self.wf_info = IWorkflowInfo(self.context)
362        return
363
364    @action(_('Save'), style='primary')
365    def save(self, **data):
366        form = self.request.form
367        password = form.get('password', None)
368        password_ctl = form.get('control_password', None)
369        if password:
370            validator = getUtility(IPasswordValidator)
371            errors = validator.validate_password(password, password_ctl)
372            if errors:
[11985]373                self.flash(' '.join(errors), type="danger")
[11967]374                return
375        changed_fields = self.applyData(self.context, **data)
376        # Turn list of lists into single list
377        if changed_fields:
378            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
379        else:
380            changed_fields = []
381        if password:
382            # Now we know that the form has no errors and can set password
383            IUserAccount(self.context).setPassword(password)
384            changed_fields.append('password')
385        fields_string = ' + '.join(changed_fields)
386        self.flash(_('Form has been saved.'))
387        if fields_string:
388            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
389        return
390
[11985]391
[11967]392class CustomerTriggerTransitionFormPage(IkobaEditFormPage):
[12028]393    """ View to trigger customer workflow transitions
[11967]394    """
395    grok.context(ICustomer)
396    grok.name('trigtrans')
397    grok.require('waeup.triggerTransition')
398    grok.template('trigtrans')
399    label = _('Trigger registration transition')
400    pnav = 4
401
402    def getTransitions(self):
403        """Return a list of dicts of allowed transition ids and titles.
404
405        Each list entry provides keys ``name`` and ``title`` for
406        internal name and (human readable) title of a single
407        transition.
408        """
409        wf_info = IWorkflowInfo(self.context)
410        allowed_transitions = [t for t in wf_info.getManualTransitions()]
411        return [dict(name='', title=_('No transition'))] +[
412            dict(name=x, title=y) for x, y in allowed_transitions]
413
414    @action(_('Save'), style='primary')
415    def save(self, **data):
416        form = self.request.form
417        if 'transition' in form and form['transition']:
418            transition_id = form['transition']
419            wf_info = IWorkflowInfo(self.context)
420            wf_info.fireTransition(transition_id)
421        return
422
[11985]423
[11967]424class CustomerActivatePage(UtilityView, grok.View):
425    """ Activate customer account
426    """
427    grok.context(ICustomer)
428    grok.name('activate')
429    grok.require('waeup.manageCustomer')
430
431    def update(self):
432        self.context.suspended = False
433        self.context.writeLogMessage(self, 'account activated')
434        history = IObjectHistory(self.context)
435        history.addMessage('Customer account activated')
436        self.flash(_('Customer account has been activated.'))
437        self.redirect(self.url(self.context))
438        return
439
440    def render(self):
441        return
442
[11985]443
[11967]444class CustomerDeactivatePage(UtilityView, grok.View):
445    """ Deactivate customer account
446    """
447    grok.context(ICustomer)
448    grok.name('deactivate')
449    grok.require('waeup.manageCustomer')
450
451    def update(self):
452        self.context.suspended = True
453        self.context.writeLogMessage(self, 'account deactivated')
454        history = IObjectHistory(self.context)
455        history.addMessage('Customer account deactivated')
456        self.flash(_('Customer account has been deactivated.'))
457        self.redirect(self.url(self.context))
458        return
459
460    def render(self):
461        return
462
[11985]463
[11967]464class CustomerHistoryPage(IkobaPage):
465    """ Page to display customer history
466    """
467    grok.context(ICustomer)
468    grok.name('history')
469    grok.require('waeup.viewCustomer')
470    grok.template('customerhistory')
471    pnav = 4
472
473    @property
474    def label(self):
[11985]475        return _('${a}: History', mapping={'a':self.context.display_fullname})
[11967]476
[11985]477
[11967]478class CustomerRequestPasswordPage(IkobaAddFormPage):
[12039]479    """Captcha'd password request page for customers.
[11967]480    """
481    grok.name('requestpw')
482    grok.require('waeup.Anonymous')
483    grok.template('requestpw')
[12039]484    form_fields = grok.AutoFields(ICustomerRequestPW)
[11967]485    label = _('Request password for first-time login')
486
487    def update(self):
488        # Handle captcha
489        self.captcha = getUtility(ICaptchaManager).getCaptcha()
490        self.captcha_result = self.captcha.verify(self.request)
491        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
492        return
493
494    def _redirect(self, email, password, customer_id):
[12039]495        # Forward only email address to landing page in base package.
[11967]496        self.redirect(self.url(self.context, 'requestpw_complete',
[11985]497            data=dict(email=email)))
[11967]498        return
499
500    def _pw_used(self):
501        # XXX: False if password has not been used. We need an extra
502        #      attribute which remembers if customer logged in.
503        return True
504
505    @action(_('Send login credentials to email address'), style='primary')
506    def get_credentials(self, **data):
507        if not self.captcha_result.is_valid:
508            # Captcha will display error messages automatically.
509            # No need to flash something.
510            return
511        number = data.get('number','')
512        firstname = data.get('firstname','')
513        cat = getUtility(ICatalog, name='customers_catalog')
514        results = list(
515            cat.searchResults(reg_number=(number, number)))
516        if results:
517            customer = results[0]
518            if getattr(customer,'firstname',None) is None:
519                self.flash(_('An error occurred.'), type="danger")
520                return
521            elif customer.firstname.lower() != firstname.lower():
522                # Don't tell the truth here. Anonymous must not
523                # know that a record was found and only the firstname
524                # verification failed.
525                self.flash(_('No customer record found.'), type="warning")
526                return
527            elif customer.password is not None and self._pw_used:
528                self.flash(_('Your password has already been set and used. '
529                             'Please proceed to the login page.'),
530                           type="warning")
531                return
532            # Store email address but nothing else.
533            customer.email = data['email']
534            notify(grok.ObjectModifiedEvent(customer))
535        else:
536            # No record found, this is the truth.
537            self.flash(_('No customer record found.'), type="warning")
538            return
539
[11979]540        ikoba_utils = getUtility(IIkobaUtils)
541        password = ikoba_utils.genPassword()
[11967]542        mandate = PasswordMandate()
543        mandate.params['password'] = password
544        mandate.params['user'] = customer
545        site = grok.getSite()
546        site['mandates'].addMandate(mandate)
547        # Send email with credentials
548        args = {'mandate_id':mandate.mandate_id}
549        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
550        url_info = u'Confirmation link: %s' % mandate_url
551        msg = _('You have successfully requested a password for the')
[11979]552        if ikoba_utils.sendCredentials(IUserAccount(customer),
[11967]553            password, url_info, msg):
554            email_sent = customer.email
555        else:
556            email_sent = None
557        self._redirect(email=email_sent, password=password,
558            customer_id=customer.customer_id)
[11977]559        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[11967]560        self.context.logger.info(
561            '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent))
562        return
563
[11985]564
[12039]565class CustomerCreateAccountPage(IkobaAddFormPage):
566    """Captcha'd account creation page for customers.
567    """
568    grok.name('createaccount')
569    grok.require('waeup.Anonymous')
570    grok.template('createaccount')
571    form_fields = grok.AutoFields(ICustomerCreate)
572    label = _('Create customer account')
573
574    def update(self):
575        # Handle captcha
576        self.captcha = getUtility(ICaptchaManager).getCaptcha()
577        self.captcha_result = self.captcha.verify(self.request)
578        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
579        return
580
581    def _redirect(self, email, password, customer_id):
582        # Forward only email address to landing page in base package.
583        self.redirect(self.url(self.context, 'requestpw_complete',
584            data=dict(email=email)))
585        return
586
587    @action(_('Send login credentials to email address'), style='primary')
588    def create_account(self, **data):
589        if not self.captcha_result.is_valid:
590            # Captcha will display error messages automatically.
591            # No need to flash something.
592            return
593        customer = createObject(u'waeup.Customer')
594        customer.firstname = data.get('firstname','')
595        customer.middlename = data.get('middlename','')
596        customer.lastname = data.get('lastname','')
597        customer.email = data.get('email','')
598        self.context['customers'].addCustomer(customer)
599        ikoba_utils = getUtility(IIkobaUtils)
600        password = ikoba_utils.genPassword()
601        mandate = PasswordMandate()
602        mandate.params['password'] = password
603        mandate.params['user'] = customer
604        site = grok.getSite()
605        site['mandates'].addMandate(mandate)
606        # Send email with credentials
607        args = {'mandate_id':mandate.mandate_id}
608        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
609        url_info = u'Confirmation link: %s' % mandate_url
610        msg = _('You have successfully created a customer account for the')
611        if ikoba_utils.sendCredentials(IUserAccount(customer),
612            password, url_info, msg):
613            email_sent = customer.email
614        else:
615            email_sent = None
616        self._redirect(email=email_sent, password=password,
617            customer_id=customer.customer_id)
618        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
619        self.context.logger.info(
620            '%s - %s - %s' % (ob_class, customer.customer_id, email_sent))
621        return
622
623
[11967]624class CustomerRequestPasswordEmailSent(IkobaPage):
625    """Landing page after successful password request.
626
627    """
628    grok.name('requestpw_complete')
629    grok.require('waeup.Public')
630    grok.template('requestpwmailsent')
[12039]631    label = _('Your request was successful.')
[11967]632
633    def update(self, email=None, customer_id=None, password=None):
634        self.email = email
635        self.password = password
636        self.customer_id = customer_id
[11971]637        return
638
[11985]639
[11971]640class CustomerFilesUploadPage(IkobaPage):
641    """ View to upload files by customer
642    """
643    grok.context(ICustomer)
644    grok.name('change_portrait')
645    grok.require('waeup.uploadCustomerFile')
646    grok.template('filesuploadpage')
647    label = _('Upload files')
648    pnav = 4
649
650    def update(self):
[12088]651        CUSTMANAGE_STATES = getUtility(
652            ICustomersUtils).CUSTMANAGE_CUSTOMER_STATES
[12018]653        if self.context.customer.state not in CUSTMANAGE_STATES:
[11971]654            emit_lock_message(self)
655            return
656        super(CustomerFilesUploadPage, self).update()
657        return
658
[12015]659# Pages for customers
[11971]660
[11985]661
[11971]662class CustomerBaseEditFormPage(IkobaEditFormPage):
663    """ View to edit customer base data
664    """
665    grok.context(ICustomer)
666    grok.name('edit_base')
667    grok.require('waeup.handleCustomer')
668    form_fields = grok.AutoFields(ICustomer).select(
669        'email', 'phone')
670    label = _('Edit base data')
671    pnav = 4
672
673    @action(_('Save'), style='primary')
674    def save(self, **data):
675        msave(self, **data)
676        return
677
[11985]678
[11971]679class CustomerChangePasswordPage(IkobaEditFormPage):
680    """ View to edit customer passords
681    """
682    grok.context(ICustomer)
[11977]683    grok.name('changepassword')
[11971]684    grok.require('waeup.handleCustomer')
[11977]685    grok.template('changepassword')
[11971]686    label = _('Change password')
687    pnav = 4
688
689    @action(_('Save'), style='primary')
690    def save(self, **data):
691        form = self.request.form
692        password = form.get('change_password', None)
693        password_ctl = form.get('change_password_repeat', None)
694        if password:
695            validator = getUtility(IPasswordValidator)
696            errors = validator.validate_password(password, password_ctl)
697            if not errors:
698                IUserAccount(self.context).setPassword(password)
699                self.context.writeLogMessage(self, 'saved: password')
700                self.flash(_('Password changed.'))
701            else:
[11985]702                self.flash(' '.join(errors), type="warning")
[11971]703        return
[12015]704
[12051]705class CustomerBasePDFFormPage(IkobaDisplayFormPage):
706    """ Page to display customer base data in pdf files.
707    """
708
709    def __init__(self, context, request, omit_fields=()):
710        self.omit_fields = omit_fields
711        super(CustomerBasePDFFormPage, self).__init__(context, request)
712
713    @property
714    def form_fields(self):
715        form_fields = grok.AutoFields(ICustomer)
716        for field in self.omit_fields:
717            form_fields = form_fields.omit(field)
718        return form_fields
719
[12015]720# Pages for customer documents
721
722class DocumentsBreadcrumb(Breadcrumb):
723    """A breadcrumb for the documents container.
724    """
725    grok.context(ICustomerDocumentsContainer)
726    title = _('Documents')
727
728
729class DocumentBreadcrumb(Breadcrumb):
730    """A breadcrumb for the customer container.
731    """
732    grok.context(ICustomerDocument)
733
734    @property
735    def title(self):
736        return self.context.document_id
737
738
739class DocumentsManageFormPage(IkobaEditFormPage):
740    """ Page to manage the customer documents
741
742    This manage form page is for both customers and customers officers.
743    """
744    grok.context(ICustomerDocumentsContainer)
745    grok.name('index')
746    grok.require('waeup.viewCustomer')
747    form_fields = grok.AutoFields(ICustomerDocumentsContainer)
748    grok.template('documentsmanagepage')
749    pnav = 4
750
751    @property
752    def manage_documents_allowed(self):
753        return checkPermission('waeup.editCustomerDocuments', self.context)
754
755    def unremovable(self, document):
756        usertype = getattr(self.request.principal, 'user_type', None)
757        if not usertype:
758            return False
759        if not self.manage_documents_allowed:
760            return True
761        return (self.request.principal.user_type == 'customer' and \
[12089]762            document.state in (VERIFIED, REJECTED, EXPIRED))
[12015]763
764    @property
765    def label(self):
766        return _('${a}: Documents',
767            mapping = {'a':self.context.__parent__.display_fullname})
768
769    @jsaction(_('Remove selected documents'))
770    def delDocument(self, **data):
771        form = self.request.form
772        if 'val_id' in form:
773            child_id = form['val_id']
774        else:
775            self.flash(_('No document selected.'), type="warning")
776            self.redirect(self.url(self.context))
777            return
778        if not isinstance(child_id, list):
779            child_id = [child_id]
780        deleted = []
781        for id in child_id:
782            # Customers are not allowed to remove used documents
783            document = self.context.get(id, None)
784            if document is not None and not self.unremovable(document):
785                del self.context[id]
786                deleted.append(id)
787        if len(deleted):
788            self.flash(_('Successfully removed: ${a}',
789                mapping = {'a': ', '.join(deleted)}))
790            self.context.writeLogMessage(
791                self,'removed: %s' % ', '.join(deleted))
792        self.redirect(self.url(self.context))
793        return
794
795
796class DocumentAddFormPage(IkobaAddFormPage):
797    """ Page to add an document
798    """
799    grok.context(ICustomerDocumentsContainer)
800    grok.name('adddoc')
801    grok.template('documentaddform')
802    grok.require('waeup.editCustomerDocuments')
803    form_fields = grok.AutoFields(ICustomerDocument)
804    label = _('Add document')
805    pnav = 4
806
807    @property
808    def selectable_doctypes(self):
[12053]809        doctypes = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT
[12015]810        return sorted(doctypes.items())
811
812    @action(_('Create document'), style='primary')
813    def createDocument(self, **data):
814        form = self.request.form
815        customer = self.context.__parent__
816        doctype = form.get('doctype', None)
817        # Here we can create various instances of CustomerDocument derived
818        # classes depending on the doctype parameter given in form.
[12053]819        document = createObject('waeup.%s' % doctype)
[12015]820        self.context.addDocument(document)
[12053]821        doctype = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT[doctype]
[12015]822        self.flash(_('${a} created.',
823            mapping = {'a': doctype}))
824        self.context.writeLogMessage(
825            self,'added: %s %s' % (doctype, document.document_id))
826        self.redirect(self.url(self.context))
827        return
828
829    @action(_('Cancel'), validator=NullValidator)
830    def cancel(self, **data):
831        self.redirect(self.url(self.context))
832
833
834class DocumentDisplayFormPage(IkobaDisplayFormPage):
835    """ Page to view a document
836    """
837    grok.context(ICustomerDocument)
838    grok.name('index')
839    grok.require('waeup.viewCustomer')
[12016]840    grok.template('documentpage')
[12055]841    form_fields = grok.AutoFields(ICustomerDocument).omit('last_transition_date')
[12015]842    pnav = 4
843
[12018]844    #@property
845    #def label(self):
846    #    return _('${a}: Document ${b}', mapping = {
847    #        'a':self.context.customer.display_fullname,
848    #        'b':self.context.document_id})
849
[12015]850    @property
851    def label(self):
[12018]852        return _('${a}', mapping = {'a':self.context.title})
[12015]853
854
855class DocumentManageFormPage(IkobaEditFormPage):
856    """ Page to edit a document
857    """
858    grok.context(ICustomerDocument)
859    grok.name('manage')
[12016]860    grok.require('waeup.manageCustomer')
[12018]861    grok.template('documenteditpage')
[12053]862    form_fields = grok.AutoFields(ICustomerDocument).omit('last_transition_date')
[12015]863    pnav = 4
[12035]864    deletion_warning = _('Are you sure?')
[12015]865
[12018]866    #@property
867    #def label(self):
868    #    return _('${a}: Document ${b}', mapping = {
869    #        'a':self.context.customer.display_fullname,
870    #        'b':self.context.document_id})
871
[12015]872    @property
873    def label(self):
[12018]874        return _('${a}', mapping = {'a':self.context.title})
[12016]875
876    @action(_('Save'), style='primary')
877    def save(self, **data):
878        msave(self, **data)
[12018]879        return
880
[12028]881
[12018]882class DocumentEditFormPage(DocumentManageFormPage):
883    """ Page to edit a document
884    """
885    grok.name('edit')
886    grok.require('waeup.handleCustomer')
887
888    def update(self):
889        if not self.context.is_editable:
890            emit_lock_message(self)
891            return
[12028]892        return super(DocumentEditFormPage, self).update()
893
[12034]894    @action(_('Save'), style='primary')
895    def save(self, **data):
896        msave(self, **data)
897        return
[12028]898
[12034]899    @action(_('Final Submit'), warning=WARNING)
900    def finalsubmit(self, **data):
901        msave(self, **data)
902        IWorkflowInfo(self.context).fireTransition('submit')
903        self.flash(_('Form has been submitted.'))
904        self.redirect(self.url(self.context))
905        return
906
907
[12028]908class DocumentTriggerTransitionFormPage(IkobaEditFormPage):
909    """ View to trigger customer document transitions
910    """
911    grok.context(ICustomerDocument)
912    grok.name('trigtrans')
913    grok.require('waeup.triggerTransition')
914    grok.template('trigtrans')
915    label = _('Trigger document transition')
916    pnav = 4
917
918    def update(self):
919        return super(IkobaEditFormPage, self).update()
920
921    def getTransitions(self):
922        """Return a list of dicts of allowed transition ids and titles.
923
924        Each list entry provides keys ``name`` and ``title`` for
925        internal name and (human readable) title of a single
926        transition.
927        """
928        wf_info = IWorkflowInfo(self.context)
929        allowed_transitions = [t for t in wf_info.getManualTransitions()]
930        return [dict(name='', title=_('No transition'))] +[
931            dict(name=x, title=y) for x, y in allowed_transitions]
932
933    @action(_('Save'), style='primary')
934    def save(self, **data):
935        form = self.request.form
936        if 'transition' in form and form['transition']:
937            transition_id = form['transition']
938            wf_info = IWorkflowInfo(self.context)
939            wf_info.fireTransition(transition_id)
940        return
[12051]941
[12062]942class PDFDocumentsOverviewPage(UtilityView, grok.View):
[12051]943    """Deliver an overview slip.
944    """
[12059]945    grok.context(ICustomerDocumentsContainer)
[12091]946    grok.name('documents_overview_slip.pdf')
[12051]947    grok.require('waeup.viewCustomer')
948    prefix = 'form'
949
[12055]950    omit_fields = ('suspended', 'sex',
951                   'suspended_comment',)
[12051]952
953    form_fields = None
954
955    @property
956    def label(self):
957        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
958        return translate(_('Documents of'),
959            'waeup.ikoba', target_language=portal_language) \
[12059]960            + ' %s' % self.context.customer.display_fullname
[12051]961
[12052]962    @property
963    def tabletitle(self):
964        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
965        tabletitle = []
966        tabletitle.append(translate(_('Customer Documents'), 'waeup.ikoba',
967            target_language=portal_language))
968        return tabletitle
969
[12051]970    def render(self):
[12052]971        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
[12053]972        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
[12052]973        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
[12053]974        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
[12052]975        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
[12053]976        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
977        tableheader = []
[12052]978        tabledata = []
979        contenttitle = []
980        for i in range(1,3):
981            tabledata.append(sorted(
[12059]982                [value for value in self.context.values()]))
[12053]983            tableheader.append([(Id, 'document_id', 2),
984                             (Title, 'title', 6),
[12056]985                             (Type, 'translated_class_name', 6),
[12053]986                             (State, 'translated_state', 2),
987                             (LT, 'formatted_transition_date', 3),
[12052]988                             ])
[12059]989        customerview = CustomerBasePDFFormPage(self.context.customer,
[12051]990            self.request, self.omit_fields)
991        customers_utils = getUtility(ICustomersUtils)
992        return customers_utils.renderPDF(
993            self, 'overview_slip.pdf',
[12059]994            self.context.customer, customerview,
[12052]995            tableheader=tableheader,
996            tabledata=tabledata,
[12051]997            omit_fields=self.omit_fields)
[12062]998
999
1000class PDFDocumentSlipPage(UtilityView, grok.View):
1001    """Deliver pdf file including metadata.
1002    """
1003    grok.context(ICustomerDocument)
1004    grok.name('document_slip.pdf')
1005    grok.require('waeup.viewCustomer')
1006    prefix = 'form'
1007
1008    omit_fields = ('suspended', 'sex',
1009                   'suspended_comment',)
1010
1011    #form_fields = grok.AutoFields(ICustomerPDFDocument).omit(
1012    #    'last_transition_date')
1013    form_fields =()
1014
1015    @property
1016    def label(self):
1017        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1018        return '%s of %s\nTitle: %s' % (
1019            self.context.translated_class_name,
1020            self.context.customer.display_fullname,
1021            self.context.title)
1022
1023    def render(self):
1024        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1025        customerview = CustomerBasePDFFormPage(self.context.customer,
1026            self.request, self.omit_fields)
1027        customers_utils = getUtility(ICustomersUtils)
1028        return customers_utils.renderPDF(
1029            self, 'document_slip.pdf',
1030            self.context.customer, customerview,
[12090]1031            omit_fields=self.omit_fields)
1032
[12097]1033# Pages for customer contracts
[12090]1034
[12097]1035class ContractsBreadcrumb(Breadcrumb):
1036    """A breadcrumb for the contracts container.
[12090]1037    """
[12097]1038    grok.context(IContractsContainer)
1039    title = _('Contracts')
[12090]1040
1041
[12097]1042class ContractBreadcrumb(Breadcrumb):
[12090]1043    """A breadcrumb for the customer container.
1044    """
[12097]1045    grok.context(IContract)
[12090]1046
1047    @property
1048    def title(self):
[12097]1049        return self.context.contract_id
[12090]1050
1051
[12097]1052class ContractsManageFormPage(IkobaEditFormPage):
1053    """ Page to manage the customer contracts
[12090]1054
[12091]1055    This manage form page is for both customers and officers.
[12090]1056    """
[12097]1057    grok.context(IContractsContainer)
[12090]1058    grok.name('index')
1059    grok.require('waeup.viewCustomer')
[12097]1060    form_fields = grok.AutoFields(IContractsContainer)
1061    grok.template('contractsmanagepage')
[12090]1062    pnav = 4
1063
1064    @property
[12097]1065    def manage_contracts_allowed(self):
1066        return checkPermission('waeup.editContracts', self.context)
[12090]1067
[12097]1068    def unremovable(self, contract):
[12090]1069        usertype = getattr(self.request.principal, 'user_type', None)
1070        if not usertype:
1071            return False
[12097]1072        if not self.manage_contracts_allowed:
[12090]1073            return True
1074        return (self.request.principal.user_type == 'customer' and \
[12097]1075            contract.state in (APPROVED, REJECTED, EXPIRED))
[12090]1076
1077    @property
1078    def label(self):
[12097]1079        return _('${a}: Contracts',
[12090]1080            mapping = {'a':self.context.__parent__.display_fullname})
1081
[12097]1082    @jsaction(_('Remove selected contracts'))
1083    def delContract(self, **data):
[12090]1084        form = self.request.form
1085        if 'val_id' in form:
1086            child_id = form['val_id']
1087        else:
[12097]1088            self.flash(_('No contract selected.'), type="warning")
[12090]1089            self.redirect(self.url(self.context))
1090            return
1091        if not isinstance(child_id, list):
1092            child_id = [child_id]
1093        deleted = []
1094        for id in child_id:
[12097]1095            # Customers are not allowed to remove used contracts
1096            contract = self.context.get(id, None)
1097            if contract is not None and not self.unremovable(contract):
[12090]1098                del self.context[id]
1099                deleted.append(id)
1100        if len(deleted):
1101            self.flash(_('Successfully removed: ${a}',
1102                mapping = {'a': ', '.join(deleted)}))
1103            self.context.writeLogMessage(
1104                self,'removed: %s' % ', '.join(deleted))
1105        self.redirect(self.url(self.context))
1106        return
1107
1108
[12097]1109class ContractAddFormPage(IkobaAddFormPage):
1110    """ Page to add an contract
[12090]1111    """
[12097]1112    grok.context(IContractsContainer)
[12090]1113    grok.name('addapp')
[12097]1114    grok.template('contractaddform')
1115    grok.require('waeup.editContracts')
1116    form_fields = grok.AutoFields(IContract)
1117    label = _('Add contract')
[12090]1118    pnav = 4
1119
1120    @property
[12099]1121    def selectable_contypes(self):
1122        contypes = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT
1123        return sorted(contypes.items())
[12090]1124
[12097]1125    @action(_('Create contract'), style='primary')
1126    def createContract(self, **data):
[12090]1127        form = self.request.form
1128        customer = self.context.__parent__
[12112]1129        contype = form.get('contype', None)
[12097]1130        # Here we can create various instances of Contract derived
[12112]1131        # classes depending on the contype parameter given in form.
1132        contract = createObject('waeup.%s' % contype)
[12097]1133        self.context.addContract(contract)
[12112]1134        contype = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT[contype]
[12090]1135        self.flash(_('${a} created.',
[12112]1136            mapping = {'a': contype}))
[12090]1137        self.context.writeLogMessage(
[12112]1138            self,'added: %s %s' % (contype, contract.contract_id))
[12090]1139        self.redirect(self.url(self.context))
1140        return
1141
1142    @action(_('Cancel'), validator=NullValidator)
1143    def cancel(self, **data):
1144        self.redirect(self.url(self.context))
1145
1146
[12097]1147class ContractDisplayFormPage(IkobaDisplayFormPage):
1148    """ Page to view a contract
[12090]1149    """
[12097]1150    grok.context(IContract)
[12090]1151    grok.name('index')
1152    grok.require('waeup.viewCustomer')
[12097]1153    grok.template('contractpage')
[12090]1154    pnav = 4
1155
1156    @property
[12103]1157    def form_fields(self):
[12119]1158        form_fields = grok.AutoFields(self.context.form_fields_interface).omit(
[12103]1159            'last_transition_date')
[12119]1160        for field in form_fields:
1161            if field.__name__.endswith('_object'):
1162                form_fields[field.__name__].custom_widget = HREFDisplayWidget
1163        return form_fields
[12103]1164
1165    @property
[12090]1166    def label(self):
1167        return _('${a}', mapping = {'a':self.context.title})
1168
1169
[12097]1170class ContractManageFormPage(IkobaEditFormPage):
1171    """ Page to edit a contract
[12090]1172    """
[12097]1173    grok.context(IContract)
[12090]1174    grok.name('manage')
1175    grok.require('waeup.manageCustomer')
[12097]1176    grok.template('contracteditpage')
[12090]1177    pnav = 4
1178    deletion_warning = _('Are you sure?')
1179
1180    @property
[12103]1181    def form_fields(self):
1182        return grok.AutoFields(self.context.form_fields_interface).omit(
1183            'last_transition_date')
1184
1185    @property
[12090]1186    def label(self):
1187        return _('${a}', mapping = {'a':self.context.title})
1188
1189    @action(_('Save'), style='primary')
1190    def save(self, **data):
1191        msave(self, **data)
1192        return
1193
1194
[12097]1195class ContractEditFormPage(ContractManageFormPage):
1196    """ Page to edit a contract
[12090]1197    """
1198    grok.name('edit')
1199    grok.require('waeup.handleCustomer')
1200
[12103]1201    @property
1202    def form_fields(self):
1203        return grok.AutoFields(self.context.edit_form_fields_interface).omit(
1204            'last_transition_date')
1205
[12090]1206    def update(self):
1207        if not self.context.is_editable:
1208            emit_lock_message(self)
1209            return
[12097]1210        return super(ContractEditFormPage, self).update()
[12090]1211
1212    @action(_('Save'), style='primary')
1213    def save(self, **data):
1214        msave(self, **data)
1215        return
1216
[12094]1217    @action(_('Apply now (final submit)'), warning=WARNING)
[12090]1218    def finalsubmit(self, **data):
1219        msave(self, **data)
1220        IWorkflowInfo(self.context).fireTransition('submit')
1221        self.flash(_('Form has been submitted.'))
1222        self.redirect(self.url(self.context))
1223        return
1224
1225
[12097]1226class ContractTriggerTransitionFormPage(IkobaEditFormPage):
1227    """ View to trigger customer contract transitions
[12090]1228    """
[12097]1229    grok.context(IContract)
[12090]1230    grok.name('trigtrans')
1231    grok.require('waeup.triggerTransition')
1232    grok.template('trigtrans')
[12097]1233    label = _('Trigger contract transition')
[12090]1234    pnav = 4
1235
1236    def update(self):
1237        return super(IkobaEditFormPage, self).update()
1238
1239    def getTransitions(self):
1240        """Return a list of dicts of allowed transition ids and titles.
1241
1242        Each list entry provides keys ``name`` and ``title`` for
1243        internal name and (human readable) title of a single
1244        transition.
1245        """
1246        wf_info = IWorkflowInfo(self.context)
1247        allowed_transitions = [t for t in wf_info.getManualTransitions()]
1248        return [dict(name='', title=_('No transition'))] +[
1249            dict(name=x, title=y) for x, y in allowed_transitions]
1250
1251    @action(_('Save'), style='primary')
1252    def save(self, **data):
1253        form = self.request.form
1254        if 'transition' in form and form['transition']:
1255            transition_id = form['transition']
1256            wf_info = IWorkflowInfo(self.context)
1257            wf_info.fireTransition(transition_id)
1258        return
1259
[12097]1260class PDFContractsOverviewPage(UtilityView, grok.View):
[12090]1261    """Deliver an overview slip.
1262    """
[12097]1263    grok.context(IContractsContainer)
1264    grok.name('contracts_overview_slip.pdf')
[12090]1265    grok.require('waeup.viewCustomer')
1266    prefix = 'form'
1267
1268    omit_fields = ('suspended', 'sex',
1269                   'suspended_comment',)
1270
1271    form_fields = None
1272
1273    @property
1274    def label(self):
1275        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
[12097]1276        return translate(_('Contracts of'),
[12090]1277            'waeup.ikoba', target_language=portal_language) \
1278            + ' %s' % self.context.customer.display_fullname
1279
1280    @property
1281    def tabletitle(self):
1282        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1283        tabletitle = []
[12097]1284        tabletitle.append(translate(_('Customer Contracts'), 'waeup.ikoba',
[12090]1285            target_language=portal_language))
1286        return tabletitle
1287
1288    def render(self):
1289        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1290        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
1291        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
1292        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
1293        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
1294        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
1295        tableheader = []
1296        tabledata = []
1297        contenttitle = []
1298        for i in range(1,3):
1299            tabledata.append(sorted(
1300                [value for value in self.context.values()]))
[12097]1301            tableheader.append([(Id, 'contract_id', 2),
[12090]1302                             (Title, 'title', 6),
1303                             (Type, 'translated_class_name', 6),
1304                             (State, 'translated_state', 2),
1305                             (LT, 'formatted_transition_date', 3),
1306                             ])
1307        customerview = CustomerBasePDFFormPage(self.context.customer,
1308            self.request, self.omit_fields)
1309        customers_utils = getUtility(ICustomersUtils)
1310        return customers_utils.renderPDF(
1311            self, 'overview_slip.pdf',
1312            self.context.customer, customerview,
1313            tableheader=tableheader,
1314            tabledata=tabledata,
1315            omit_fields=self.omit_fields)
1316
1317
[12097]1318class PDFContractSlipPage(UtilityView, grok.View):
[12090]1319    """Deliver pdf file including metadata.
1320    """
[12097]1321    grok.context(IContract)
1322    grok.name('contract_slip.pdf')
[12090]1323    grok.require('waeup.viewCustomer')
1324    prefix = 'form'
1325
1326    omit_fields = ('suspended', 'sex',
1327                   'suspended_comment',)
1328
[12097]1329    #form_fields = grok.AutoFields(ICustomerPDFContract).omit(
[12090]1330    #    'last_transition_date')
1331    form_fields =()
1332
1333    @property
1334    def label(self):
1335        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1336        return '%s of %s\nTitle: %s' % (
1337            self.context.translated_class_name,
1338            self.context.customer.display_fullname,
1339            self.context.title)
1340
1341    def render(self):
1342        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1343        customerview = CustomerBasePDFFormPage(self.context.customer,
1344            self.request, self.omit_fields)
1345        customers_utils = getUtility(ICustomersUtils)
1346        return customers_utils.renderPDF(
[12097]1347            self, 'contract_slip.pdf',
[12090]1348            self.context.customer, customerview,
1349            omit_fields=self.omit_fields)
Note: See TracBrowser for help on using the repository browser.