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

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

Catch InvalidTransitionError? in UI.

  • Property svn:keywords set to Id
File size: 46.4 KB
RevLine 
[12015]1## $Id: browser.py 12151 2014-12-05 15:43:24Z 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
[12018]845    #@property
846    #def label(self):
847    #    return _('${a}: Document ${b}', mapping = {
848    #        'a':self.context.customer.display_fullname,
849    #        'b':self.context.document_id})
850
[12015]851    @property
852    def label(self):
[12018]853        return _('${a}', mapping = {'a':self.context.title})
[12015]854
855
856class DocumentManageFormPage(IkobaEditFormPage):
857    """ Page to edit a document
858    """
859    grok.context(ICustomerDocument)
860    grok.name('manage')
[12016]861    grok.require('waeup.manageCustomer')
[12018]862    grok.template('documenteditpage')
[12053]863    form_fields = grok.AutoFields(ICustomerDocument).omit('last_transition_date')
[12015]864    pnav = 4
[12035]865    deletion_warning = _('Are you sure?')
[12015]866
[12018]867    #@property
868    #def label(self):
869    #    return _('${a}: Document ${b}', mapping = {
870    #        'a':self.context.customer.display_fullname,
871    #        'b':self.context.document_id})
872
[12015]873    @property
874    def label(self):
[12018]875        return _('${a}', mapping = {'a':self.context.title})
[12016]876
877    @action(_('Save'), style='primary')
878    def save(self, **data):
879        msave(self, **data)
[12018]880        return
881
[12028]882
[12018]883class DocumentEditFormPage(DocumentManageFormPage):
884    """ Page to edit a document
885    """
886    grok.name('edit')
887    grok.require('waeup.handleCustomer')
888
889    def update(self):
890        if not self.context.is_editable:
891            emit_lock_message(self)
892            return
[12028]893        return super(DocumentEditFormPage, self).update()
894
[12034]895    @action(_('Save'), style='primary')
896    def save(self, **data):
897        msave(self, **data)
898        return
[12028]899
[12034]900    @action(_('Final Submit'), warning=WARNING)
901    def finalsubmit(self, **data):
902        msave(self, **data)
903        IWorkflowInfo(self.context).fireTransition('submit')
904        self.flash(_('Form has been submitted.'))
905        self.redirect(self.url(self.context))
906        return
907
908
[12028]909class DocumentTriggerTransitionFormPage(IkobaEditFormPage):
910    """ View to trigger customer document transitions
911    """
912    grok.context(ICustomerDocument)
913    grok.name('trigtrans')
914    grok.require('waeup.triggerTransition')
915    grok.template('trigtrans')
916    label = _('Trigger document transition')
917    pnav = 4
918
919    def update(self):
920        return super(IkobaEditFormPage, self).update()
921
922    def getTransitions(self):
923        """Return a list of dicts of allowed transition ids and titles.
924
925        Each list entry provides keys ``name`` and ``title`` for
926        internal name and (human readable) title of a single
927        transition.
928        """
929        wf_info = IWorkflowInfo(self.context)
930        allowed_transitions = [t for t in wf_info.getManualTransitions()]
931        return [dict(name='', title=_('No transition'))] +[
932            dict(name=x, title=y) for x, y in allowed_transitions]
933
934    @action(_('Save'), style='primary')
935    def save(self, **data):
936        form = self.request.form
937        if 'transition' in form and form['transition']:
938            transition_id = form['transition']
939            wf_info = IWorkflowInfo(self.context)
940            wf_info.fireTransition(transition_id)
941        return
[12051]942
[12062]943class PDFDocumentsOverviewPage(UtilityView, grok.View):
[12051]944    """Deliver an overview slip.
945    """
[12059]946    grok.context(ICustomerDocumentsContainer)
[12091]947    grok.name('documents_overview_slip.pdf')
[12051]948    grok.require('waeup.viewCustomer')
949    prefix = 'form'
950
[12055]951    omit_fields = ('suspended', 'sex',
952                   'suspended_comment',)
[12051]953
954    form_fields = None
955
956    @property
957    def label(self):
958        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
959        return translate(_('Documents of'),
960            'waeup.ikoba', target_language=portal_language) \
[12059]961            + ' %s' % self.context.customer.display_fullname
[12051]962
[12052]963    @property
964    def tabletitle(self):
965        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
966        tabletitle = []
967        tabletitle.append(translate(_('Customer Documents'), 'waeup.ikoba',
968            target_language=portal_language))
969        return tabletitle
970
[12051]971    def render(self):
[12052]972        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
[12053]973        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
[12052]974        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
[12053]975        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
[12052]976        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
[12053]977        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
978        tableheader = []
[12052]979        tabledata = []
980        contenttitle = []
981        for i in range(1,3):
982            tabledata.append(sorted(
[12059]983                [value for value in self.context.values()]))
[12053]984            tableheader.append([(Id, 'document_id', 2),
985                             (Title, 'title', 6),
[12056]986                             (Type, 'translated_class_name', 6),
[12053]987                             (State, 'translated_state', 2),
988                             (LT, 'formatted_transition_date', 3),
[12052]989                             ])
[12059]990        customerview = CustomerBasePDFFormPage(self.context.customer,
[12051]991            self.request, self.omit_fields)
992        customers_utils = getUtility(ICustomersUtils)
993        return customers_utils.renderPDF(
994            self, 'overview_slip.pdf',
[12059]995            self.context.customer, customerview,
[12052]996            tableheader=tableheader,
997            tabledata=tabledata,
[12051]998            omit_fields=self.omit_fields)
[12062]999
1000
1001class PDFDocumentSlipPage(UtilityView, grok.View):
1002    """Deliver pdf file including metadata.
1003    """
1004    grok.context(ICustomerDocument)
1005    grok.name('document_slip.pdf')
1006    grok.require('waeup.viewCustomer')
1007    prefix = 'form'
1008
1009    omit_fields = ('suspended', 'sex',
1010                   'suspended_comment',)
1011
1012    #form_fields = grok.AutoFields(ICustomerPDFDocument).omit(
1013    #    'last_transition_date')
1014    form_fields =()
1015
1016    @property
1017    def label(self):
1018        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1019        return '%s of %s\nTitle: %s' % (
1020            self.context.translated_class_name,
1021            self.context.customer.display_fullname,
1022            self.context.title)
1023
1024    def render(self):
1025        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1026        customerview = CustomerBasePDFFormPage(self.context.customer,
1027            self.request, self.omit_fields)
1028        customers_utils = getUtility(ICustomersUtils)
1029        return customers_utils.renderPDF(
1030            self, 'document_slip.pdf',
1031            self.context.customer, customerview,
[12090]1032            omit_fields=self.omit_fields)
1033
[12097]1034# Pages for customer contracts
[12090]1035
[12097]1036class ContractsBreadcrumb(Breadcrumb):
1037    """A breadcrumb for the contracts container.
[12090]1038    """
[12097]1039    grok.context(IContractsContainer)
1040    title = _('Contracts')
[12090]1041
1042
[12097]1043class ContractBreadcrumb(Breadcrumb):
[12090]1044    """A breadcrumb for the customer container.
1045    """
[12097]1046    grok.context(IContract)
[12090]1047
1048    @property
1049    def title(self):
[12097]1050        return self.context.contract_id
[12090]1051
1052
[12097]1053class ContractsManageFormPage(IkobaEditFormPage):
1054    """ Page to manage the customer contracts
[12090]1055
[12091]1056    This manage form page is for both customers and officers.
[12090]1057    """
[12097]1058    grok.context(IContractsContainer)
[12090]1059    grok.name('index')
1060    grok.require('waeup.viewCustomer')
[12097]1061    form_fields = grok.AutoFields(IContractsContainer)
1062    grok.template('contractsmanagepage')
[12090]1063    pnav = 4
1064
1065    @property
[12097]1066    def manage_contracts_allowed(self):
1067        return checkPermission('waeup.editContracts', self.context)
[12090]1068
[12097]1069    def unremovable(self, contract):
[12090]1070        usertype = getattr(self.request.principal, 'user_type', None)
1071        if not usertype:
1072            return False
[12097]1073        if not self.manage_contracts_allowed:
[12090]1074            return True
1075        return (self.request.principal.user_type == 'customer' and \
[12097]1076            contract.state in (APPROVED, REJECTED, EXPIRED))
[12090]1077
1078    @property
1079    def label(self):
[12097]1080        return _('${a}: Contracts',
[12090]1081            mapping = {'a':self.context.__parent__.display_fullname})
1082
[12097]1083    @jsaction(_('Remove selected contracts'))
1084    def delContract(self, **data):
[12090]1085        form = self.request.form
1086        if 'val_id' in form:
1087            child_id = form['val_id']
1088        else:
[12097]1089            self.flash(_('No contract selected.'), type="warning")
[12090]1090            self.redirect(self.url(self.context))
1091            return
1092        if not isinstance(child_id, list):
1093            child_id = [child_id]
1094        deleted = []
1095        for id in child_id:
[12097]1096            # Customers are not allowed to remove used contracts
1097            contract = self.context.get(id, None)
1098            if contract is not None and not self.unremovable(contract):
[12090]1099                del self.context[id]
1100                deleted.append(id)
1101        if len(deleted):
1102            self.flash(_('Successfully removed: ${a}',
1103                mapping = {'a': ', '.join(deleted)}))
1104            self.context.writeLogMessage(
1105                self,'removed: %s' % ', '.join(deleted))
1106        self.redirect(self.url(self.context))
1107        return
1108
1109
[12097]1110class ContractAddFormPage(IkobaAddFormPage):
1111    """ Page to add an contract
[12090]1112    """
[12097]1113    grok.context(IContractsContainer)
[12090]1114    grok.name('addapp')
[12097]1115    grok.template('contractaddform')
1116    grok.require('waeup.editContracts')
1117    form_fields = grok.AutoFields(IContract)
1118    label = _('Add contract')
[12090]1119    pnav = 4
1120
1121    @property
[12099]1122    def selectable_contypes(self):
1123        contypes = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT
1124        return sorted(contypes.items())
[12090]1125
[12097]1126    @action(_('Create contract'), style='primary')
1127    def createContract(self, **data):
[12090]1128        form = self.request.form
1129        customer = self.context.__parent__
[12112]1130        contype = form.get('contype', None)
[12097]1131        # Here we can create various instances of Contract derived
[12112]1132        # classes depending on the contype parameter given in form.
1133        contract = createObject('waeup.%s' % contype)
[12097]1134        self.context.addContract(contract)
[12112]1135        contype = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT[contype]
[12090]1136        self.flash(_('${a} created.',
[12112]1137            mapping = {'a': contype}))
[12090]1138        self.context.writeLogMessage(
[12112]1139            self,'added: %s %s' % (contype, contract.contract_id))
[12090]1140        self.redirect(self.url(self.context))
1141        return
1142
1143    @action(_('Cancel'), validator=NullValidator)
1144    def cancel(self, **data):
1145        self.redirect(self.url(self.context))
1146
1147
[12097]1148class ContractDisplayFormPage(IkobaDisplayFormPage):
1149    """ Page to view a contract
[12090]1150    """
[12097]1151    grok.context(IContract)
[12090]1152    grok.name('index')
1153    grok.require('waeup.viewCustomer')
[12097]1154    grok.template('contractpage')
[12090]1155    pnav = 4
1156
1157    @property
[12103]1158    def form_fields(self):
[12119]1159        form_fields = grok.AutoFields(self.context.form_fields_interface).omit(
[12103]1160            'last_transition_date')
[12119]1161        for field in form_fields:
1162            if field.__name__.endswith('_object'):
1163                form_fields[field.__name__].custom_widget = HREFDisplayWidget
1164        return form_fields
[12103]1165
1166    @property
[12090]1167    def label(self):
1168        return _('${a}', mapping = {'a':self.context.title})
1169
1170
[12097]1171class ContractManageFormPage(IkobaEditFormPage):
1172    """ Page to edit a contract
[12090]1173    """
[12097]1174    grok.context(IContract)
[12090]1175    grok.name('manage')
1176    grok.require('waeup.manageCustomer')
[12097]1177    grok.template('contracteditpage')
[12090]1178    pnav = 4
1179    deletion_warning = _('Are you sure?')
1180
1181    @property
[12103]1182    def form_fields(self):
1183        return grok.AutoFields(self.context.form_fields_interface).omit(
1184            'last_transition_date')
1185
1186    @property
[12090]1187    def label(self):
1188        return _('${a}', mapping = {'a':self.context.title})
1189
1190    @action(_('Save'), style='primary')
1191    def save(self, **data):
1192        msave(self, **data)
1193        return
1194
1195
[12097]1196class ContractEditFormPage(ContractManageFormPage):
1197    """ Page to edit a contract
[12090]1198    """
1199    grok.name('edit')
1200    grok.require('waeup.handleCustomer')
1201
[12103]1202    @property
1203    def form_fields(self):
1204        return grok.AutoFields(self.context.edit_form_fields_interface).omit(
1205            'last_transition_date')
1206
[12090]1207    def update(self):
1208        if not self.context.is_editable:
1209            emit_lock_message(self)
1210            return
[12097]1211        return super(ContractEditFormPage, self).update()
[12090]1212
1213    @action(_('Save'), style='primary')
1214    def save(self, **data):
1215        msave(self, **data)
1216        return
1217
[12094]1218    @action(_('Apply now (final submit)'), warning=WARNING)
[12090]1219    def finalsubmit(self, **data):
1220        msave(self, **data)
1221        IWorkflowInfo(self.context).fireTransition('submit')
1222        self.flash(_('Form has been submitted.'))
1223        self.redirect(self.url(self.context))
1224        return
1225
1226
[12097]1227class ContractTriggerTransitionFormPage(IkobaEditFormPage):
1228    """ View to trigger customer contract transitions
[12090]1229    """
[12097]1230    grok.context(IContract)
[12090]1231    grok.name('trigtrans')
1232    grok.require('waeup.triggerTransition')
1233    grok.template('trigtrans')
[12097]1234    label = _('Trigger contract transition')
[12090]1235    pnav = 4
1236
1237    def update(self):
1238        return super(IkobaEditFormPage, self).update()
1239
1240    def getTransitions(self):
1241        """Return a list of dicts of allowed transition ids and titles.
1242
1243        Each list entry provides keys ``name`` and ``title`` for
1244        internal name and (human readable) title of a single
1245        transition.
1246        """
1247        wf_info = IWorkflowInfo(self.context)
1248        allowed_transitions = [t for t in wf_info.getManualTransitions()]
1249        return [dict(name='', title=_('No transition'))] +[
1250            dict(name=x, title=y) for x, y in allowed_transitions]
1251
1252    @action(_('Save'), style='primary')
1253    def save(self, **data):
1254        form = self.request.form
1255        if 'transition' in form and form['transition']:
1256            transition_id = form['transition']
1257            wf_info = IWorkflowInfo(self.context)
[12151]1258            try:
1259                wf_info.fireTransition(transition_id)
1260            except InvalidTransitionError, error:
1261                self.flash(error, type="warning")
[12090]1262        return
1263
[12097]1264class PDFContractsOverviewPage(UtilityView, grok.View):
[12090]1265    """Deliver an overview slip.
1266    """
[12097]1267    grok.context(IContractsContainer)
1268    grok.name('contracts_overview_slip.pdf')
[12090]1269    grok.require('waeup.viewCustomer')
1270    prefix = 'form'
1271
1272    omit_fields = ('suspended', 'sex',
1273                   'suspended_comment',)
1274
1275    form_fields = None
1276
1277    @property
1278    def label(self):
1279        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
[12097]1280        return translate(_('Contracts of'),
[12090]1281            'waeup.ikoba', target_language=portal_language) \
1282            + ' %s' % self.context.customer.display_fullname
1283
1284    @property
1285    def tabletitle(self):
1286        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1287        tabletitle = []
[12097]1288        tabletitle.append(translate(_('Customer Contracts'), 'waeup.ikoba',
[12090]1289            target_language=portal_language))
1290        return tabletitle
1291
1292    def render(self):
1293        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1294        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
1295        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
1296        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
1297        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
1298        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
1299        tableheader = []
1300        tabledata = []
1301        contenttitle = []
1302        for i in range(1,3):
1303            tabledata.append(sorted(
1304                [value for value in self.context.values()]))
[12097]1305            tableheader.append([(Id, 'contract_id', 2),
[12090]1306                             (Title, 'title', 6),
1307                             (Type, 'translated_class_name', 6),
1308                             (State, 'translated_state', 2),
1309                             (LT, 'formatted_transition_date', 3),
1310                             ])
1311        customerview = CustomerBasePDFFormPage(self.context.customer,
1312            self.request, self.omit_fields)
1313        customers_utils = getUtility(ICustomersUtils)
1314        return customers_utils.renderPDF(
1315            self, 'overview_slip.pdf',
1316            self.context.customer, customerview,
1317            tableheader=tableheader,
1318            tabledata=tabledata,
1319            omit_fields=self.omit_fields)
1320
1321
[12097]1322class PDFContractSlipPage(UtilityView, grok.View):
[12090]1323    """Deliver pdf file including metadata.
1324    """
[12097]1325    grok.context(IContract)
1326    grok.name('contract_slip.pdf')
[12090]1327    grok.require('waeup.viewCustomer')
1328    prefix = 'form'
1329
1330    omit_fields = ('suspended', 'sex',
1331                   'suspended_comment',)
1332
[12097]1333    #form_fields = grok.AutoFields(ICustomerPDFContract).omit(
[12090]1334    #    'last_transition_date')
1335    form_fields =()
1336
1337    @property
1338    def label(self):
1339        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1340        return '%s of %s\nTitle: %s' % (
1341            self.context.translated_class_name,
1342            self.context.customer.display_fullname,
1343            self.context.title)
1344
1345    def render(self):
1346        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1347        customerview = CustomerBasePDFFormPage(self.context.customer,
1348            self.request, self.omit_fields)
1349        customers_utils = getUtility(ICustomersUtils)
1350        return customers_utils.renderPDF(
[12097]1351            self, 'contract_slip.pdf',
[12090]1352            self.context.customer, customerview,
1353            omit_fields=self.omit_fields)
Note: See TracBrowser for help on using the repository browser.