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

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

Define is_verifiable as probably requested. Adjust test.

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