svv/orders.py
author Tero Marttila <terom@fixme.fi>
Mon, 10 Jan 2011 17:51:08 +0200
changeset 53 06dad873204d
parent 48 06fa83c8c0bb
permissions -rw-r--r--
items: use DeleteItemForm for ItemView as well
# coding: utf-8
"""
    Order data model/view/handler
"""

from svv.controllers import PageHandler, DocumentHandler
from svv.forms import FormError, BaseForm, json
from svv.html import tags
from svv import database as db
from svv import pdf, markup

import datetime
import logging

log = logging.getLogger('svv.orders')

class Customer (object) :
    """
        Data-mapping for the customers table
    """
    
    # customer name (organization or natural person)
    name = None

class Contact (object) :
    """
        Data-mapping for the contacts table
    """

    # human-readable details
    name = None
    phone = None
    email = None

    # relation to Customer
    customer = None


class Order (object) :
    """
        Data-mapping for orders table.
    """

    # relation to Customer
    customer = None
    
    # relation to Contact
    contact = None
    
    # event info
    event_name = None
    event_description = None
    event_start = None
    event_end = None

    def __init__ (self) :
        """
            Construct an Order with default values
        """
        
        ## Set default values
        tomorrow = datetime.date.today() + datetime.timedelta(days=1)
        
        # default to tomorrow afternoon
        self.event_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00))
        self.event_end = None   # determined by UI behaviour
            

    def format_event_time (self) :
        """
            Return a concise string describing the event's approximate time span.
        """

        start = self.event_start
        end = self.event_end

        # different days
        if (start.year, start.month) != (end.year, end.month) :
            # the full dates
            date = "%s - %s" % (start.strftime("%d.%m.%Y"), end.strftime("%d.%m.%Y"))

        elif (start.day != end.day) :
            # day range
            date = "%s-%s.%s" % (start.strftime("%d"), end.strftime("%d"), start.strftime("%m.%Y"))
        
        else : # start.date() == end.date()
            # single day
            date = start.strftime("%d.%m.%Y")

        # time
        time = "%s-%s" % (start.strftime("%H:%M"), end.strftime("%H:%M"))

        # format
        return "%s %s" % (date, time)


    def on_date (self, date) :
        """
            Does the event take place on this date?
        """

        return self.event_start.date() <= date <= self.event_end.date()

# bind against database schema
db.mapper(Customer, db.customers)
db.mapper(Contact, db.contacts)
db.mapper(Order, db.orders, properties=dict(
    customer    = db.relation(Customer),
    contact     = db.relation(Contact),
))

class OrderForm (BaseForm) :
    """
        A single instance of a <form>, where we can process submitted data from the client, storing the associated
        Order-related data, and then render a form for any of that related data.
    """

    def __init__ (self, app) :
        """
            Bind against app for database access.
        """

        super(OrderForm, self).__init__()

        self.app = app
    
    def defaults (self) :
        """
            Update our attributes with default values
        """

        self.customer_id = None
        self.customer_name = None

        self.contact_id = None
        self.contact_name = None
        self.contact_phone = None
        self.contact_email = None
        self.contact_customer = None

        self.event_name = None
        self.event_description = None

        tomorrow = datetime.date.today() + datetime.timedelta(days=1)
        
        # default to tomorrow afternoon
        self.event_start = datetime.datetime.combine(tomorrow, datetime.time(16, 00))
        
        # automatically determined once start is set
        self.event_end = None

    def process_customer (self) :
        """
            Process the incoming customer_* fields, returning (customer_id, customer_name).
        """

        try :
            customer_id = self.process_integer_field('customer_id')
            customer_name = self.process_text_field('customer_name')

            if not customer_id and not customer_name :
                raise FormError('customer_name', None, "Must enter a customer")
       
            return self.process_multifield(db.customers,
                ('customer_id', db.customers.c.id, customer_id),
                (
                    ('customer_name', db.customers.c.name, customer_name),
                ),
            )

        except FormError, e :
            # list it
            self.fail_field(e, 'customer_name')

            return None, None
 
    def process_contact (self, customer_id, customer_name) :
        """
            Process the incoming contact_* fields, returning 
                (contact_id, contact_name, contact_phone, contact_email, contact_customer)
        """
        
        try :
            contact_id = self.process_integer_field('contact_id')
            contact_name = self.process_text_field('contact_name')
            contact_phone = self.process_text_field('contact_phone')
            contact_email = self.process_text_field('contact_email')
            contact_customer = customer_id

            if not contact_id and not contact_name and (contact_phone or contact_email) and customer_name :
                # use customer name as contact name, if otherwise missing
                contact_name = customer_name

            if not contact_id and not (contact_name or contact_phone or contact_email) :
                raise FormError('contact_name', None, "Must enter a contact")

            return self.process_multifield(db.contacts,
                ('contact_id', db.contacts.c.id, contact_id),
                (
                    ('contact_name', db.contacts.c.name, contact_name),
                    ('contact_phone', db.contacts.c.phone, contact_phone),
                    ('contact_email', db.contacts.c.email, contact_email),
                    ('contact_customer', db.contacts.c.customer_id, contact_customer),
                ),
            )

        except FormError, e :
            # list it
            self.fail_field(e, 'contact_name' if e.field == 'contact_id' else None)

            return None, None, None, None, None

    def process_event (self) :
        """
            Process the incoming event_* fields, returning
                (event_name, event_description, event_start, event_end)
        """
        
        try :
            event_name = self.process_text_field('event_name')
            event_description = self.process_text_field('event_description', strip=False)
            event_start = self.process_datetime_field('event_start')
            event_end = self.process_datetime_field('event_end')

            if event_end < event_start :
                raise FormError('event_start', event_end, "Event must end after start")

            return (event_name, event_description, event_start, event_end)

        except FormError, e :
            # list it
            self.fail_field(e)

            return None, None, None, None

    def process (self, data) :
        """
            Bind ourselves to the given incoming POST data, and update our order field attributes.

                data        - the submitted POST data as a MultiDict

            Returns True if all fields were processed without errors, False otherwise.
        """

        # bind
        self.data = data

        # customer
        self.customer_id, self.customer_name = self.process_customer()

        # contact
        self.contact_id, self.contact_name, self.contact_phone, self.contact_email, self.contact_customer = self.process_contact(self.customer_id, self.customer_name)

        if self.contact_customer and not self.customer_id :
            # TODO: be really smart?
            pass

        # event
        self.event_name, self.event_description, self.event_start, self.event_end = self.process_event()
    
        return not self.errors

    def load (self, order) :
        """
            Load our field values from the given Order.
        """
        
        if order.customer :
            self.customer_id        = order.customer.id
            self.customer_name      = order.customer.name
        
        if order.contact :
            self.contact_id         = order.contact.id
            self.contact_name       = order.contact.name
            self.contact_phone      = order.contact.phone
            self.contact_email      = order.contact.email

        self.event_name         = order.event_name
        self.event_description  = order.event_description
        self.event_start        = order.event_start
        self.event_end          = order.event_end

    def build_customer_list (self) :
        """
            Query a (id, name) list of customers.
        """

        sql = db.select([db.customers.c.id, db.customers.c.name])

        return self.app.query(sql)

    def build_contact_list (self, customer_id=None) :
        """
            Query a (id, name, phone, email) list of contacts, optionally for given customer if given.
        """

        sql = db.select([db.contacts.c.id, db.contacts.c.name, db.contacts.c.phone, db.contacts.c.email])

        if customer_id :
            sql = sql.order_by((db.contacts.c.customer_id == customer_id))

        return self.app.query(sql)


    def render_customer_input (self) :
        """
            Render HTML for customer_id/name field inputs.
        """

        # all known customers
        customers = list(self.build_customer_list())
        
        return (
            self.render_select_input('customer_id', [(0, u"Luo uusi")] + customers, self.customer_id),
            self.render_text_input('customer_name', self.customer_name),

            tags.script(r"$(document).ready(function () { $('#customer_id').formSelectsPreset({textTarget: $('#customer_name')}); });"),
        )

    def render_contact_input (self) :
        """
            Render HTML for contact name field <input>s
        """
        # recommended contacts for selected customer, if known
        contacts = list(self.build_contact_list(self.customer_id))

        return (
            self.render_select_input('contact_id', [(0, u"Luo uusi")] + [(id, name) for id, name, phone, email in contacts], self.contact_id),
            self.render_text_input('contact_name', self.contact_name),

            tags.script("""\
$(document).ready(function () { 
    contact_phones = %(phones)s;
    contact_emails = %(emails)s;

    $('#contact_id').formSelectsPreset({
        textTarget: $('#contact_name'),

        mapTargets: [
            [$('#contact_phone'), contact_phones],
            [$('#contact_email'), contact_emails]
        ]
    });
});
""" %       dict(
                phones  = json.dumps(dict((row[db.contacts.c.id], row[db.contacts.c.phone]) for row in contacts)),
                emails  = json.dumps(dict((row[db.contacts.c.id], row[db.contacts.c.email]) for row in contacts)),
            )),
        )

    def render_event_input (self) :
        """
            Render HTML for event start/end field <input>s
        """
        
        return (
            self.render_datetime_input('event_start', self.event_start),
            " - ",
            self.render_datetime_input('event_end', self.event_end),

            tags.script(r"""
$(document).ready(function () { 
    var event_start = $('#event_start');
    var event_end = $('#event_end');
    
    // XXX: datetimepicker breaks beforeShow completely (event having one breaks)
    event_start.change(function () {
        // copy value as default
        var start_date = event_start.datetimepicker("getDate");
        
        if (start_date) {
            event_end.datetimepicker("option", {
                defaultDate: start_date,
                minDate: start_date,
            });
        }
    });

    // init default as well
    event_start.change();
});"""      ),

        )

    def render (self, action, submit=u"Tallenna", return_url=None) :
        """
            Render the entire <form>, using any loaded/processed values.

                action          - the target URL for the form to POST to
                submit          - label for the submit button
                return_url      - URL to return to if form is abandoned
        """

        return tags.form(action=action, method='POST')(
            (
                tags.h3(u"Lomakkeessa oli virheitä"),
                tags.p(u"Korjaa lomake ja lähetä uudelleen"),

                tags.ul(class_='errors')(
                    tags.li(tags.a(href='#' + error.field)(
                        tags.em(error.field),
                        error.message,
                    )) for field_errors in self.errors.itervalues() for error in field_errors
                ),
            ) if self.errors else None,

            tags.fieldset(
                tags.legend(u"Tilaaja"),
                
                tags.ol(
                    self.render_form_field('customer_name', u"Tilaaja", u"Tilaavan yhdistyksen/henkilön nimi", (
                        self.render_customer_input()
                    )),

                    self.render_form_field('contact_name', u"Yhteyshenkilö", u"Yhteyshenkilön nimi, jos eri kun tilaaja", (
                        self.render_contact_input()
                    )),

                    self.render_form_field('contact_phone', u"Puhelinnumero", u"Yhteyshenkilön puhelinnumero", (
                        self.render_text_input('contact_phone', self.contact_phone)
                    )),

                    self.render_form_field('contact_email', u"Sähköpostiosoite", u"Yhteyshenkilön sähköpostiosoite", (
                        self.render_text_input('contact_email', self.contact_email)
                    )),
                ),
            ),

            tags.fieldset(
                tags.legend(u"Tapahtuma"),
           
                tags.ol(
                    self.render_form_field('event_name', u"Tapahtuma", u"Tapahtuman lyhyt nimi", (
                        self.render_text_input('event_name', self.event_name)
                    )),

                    self.render_form_field('event_description', u"Lisätiedot", u"Tapahtuman tarkemmat tiedot", (
                        self.render_text_input('event_description', self.event_description, multiline=True)
                    )),

                    self.render_form_field('event_start', u"Ajankohta", 
                        (
                            u"Tapahtuman ajankohta (kamat noudetaan - palautetaan)", 
                            tags.br(), 
                            u"(%s)" % (datetime.datetime.now().strftime(self.DATETIME_FORMAT)),
                        ), 
                        (self.render_event_input()),
                    ),
                ),
            ),

            tags.input(type='submit', value=submit),
            self.render_reset_button(u"Unohda koko juttu...", return_url) if return_url else None,
        )

class OrderContractForm (BaseForm) :
    """
        Form for generating an order's contract document
    """

    DEFAULT_PLACE = u"Otaniemi"

    DEFAULT_TEXT = u"""\
# Vuokrasopimus
### Teekkarispeksi ry AV-tekniikka

## Sopimusehdot

1.  ## Osapuolet
    1.  Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”.
    2.  {order.customer.name}, jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.

2.  ## Kaluston lainaaminen
    1.  ### Yleistä
        Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle.

    2.  ### Vuokranantajan velvollisuudet
        Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja.

    3.  ### Blaa Blaa
        Etc.

3. ## Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset
    1.  ### Sopimuksen voimassaolo
        Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet.

    2.  ### Muutosten tekeminen
        Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken.

    3.  ### Blaa Blaa

        Etc.

## Nouto
    Aika: _______________       Paikka: _______________

## Palautus
    Aika: _______________       Paikka: _______________
        """

    # user clicked 'Edit' button, i.e. they want to view the rest of the edit fields
    edit = None

    def defaults (self) :

        today = datetime.date.today()

        self.prefill_placetime = False
        self.prefill_placetime_default = "%s, %s" % (self.DEFAULT_PLACE, today.strftime("%d.%m.%Y"))
        self.prefill_ourname = False
        self.prefill_ourname_default = None
        
        self.edit = False

        self.contract_text = self.DEFAULT_TEXT

    def render (self, action, return_url=None, edit_button=False) :
        """
                return_ulr              - display a "Return to order" link/button TO THIS URL
                edit_button             - display an "Edit further" button, hiding extra fields until pressed
        """
        
        # self.url_for(OrderContractDocument, id=id)
        return tags.form(action=action, method='POST')(
            tags.fieldset(
                tags.legend(u"Vuokrasopimus"),

                tags.ol(
                    self.render_form_field('prefill_placetime', u"Esitäytä aika/paikka", u"Esitäytä allekirjoitusosion aika/paikka rivi", (
                        self.render_armed_text_input('prefill_placetime', self.prefill_placetime, self.prefill_placetime_default)
                    )),

                    self.render_form_field('prefill_ourname', u"Esitäytä vuokranantaja", u"Esitäytä allekirjoitusosion vuokranantajan nimi", (
                        self.render_armed_text_input('prefill_ourname', self.prefill_ourname, self.prefill_ourname_default)
                    )),

                    (
                        self.render_form_field('contract_text', u"Sopimusteksti", u"Vuokrasopimuksen vapaamuotoinen otsikko, teksti, sopimusehdot, yms. Voidaan käyttää muuttujakenttiä (i.e. {...}). XXX: listaa kentät", (
                            self.render_text_input('contract_text', self.contract_text, multiline=True, autoscale=True)
                        )),

                    ) if not edit_button or self.edit else None,

                    tags.li(
                        self.render_submit_button(u"Tulosta vuokrasopimus"),
                        self.render_submit_button(u"Muokkaa vuokrasopimusta", 'edit') if edit_button else None,
                        self.render_reset_button(u"Unohda koko juttu...", return_url) if return_url else None,
                    ),
                ),
            ),

        )


    def process (self, data) :
        """
            Bind incoming data.

            Returns true if no error input, and not requesting the full editing form ("Edit more" button).
        """
        
        # bind
        self.data = data

        self.prefill_placetime = self.process_armed_text_field('prefill_placetime')
        self.prefill_ourname = self.process_armed_text_field('prefill_ourname')
        
        self.edit = self.process_action_field('edit')
        
        self.contract_text = self.process_text_field('contract_text', default=self.DEFAULT_TEXT)
        
        return not self.errors and not self.edit

class OrdersView (PageHandler) :
    """
        Render overview list of all orders in database.

        Currently divided into current/upcoming/past listings
    """

    def process (self) :
        """
            prepare
        """
        
        # database sesssion
        self.session = self.app.session()

    def build_orders_list (self, order_by=(Order.event_start)) :
        """
            Build summary list of orders, returning a series of Order objects, ordered by event_start, per default.
        """

        return self.session.query(Order).options(db.eagerload(Order.customer), db.eagerload(Order.contact)).order_by(order_by).all()
    
    def group_orders_by_time (self, orders) :
        """
            Sort the given set of orders into a set of categories by time.

            Returns a
                (past, current, future)
            set of order summary items.
        """

        past = []
        current = []
        future = []

        today = datetime.date.today()

        def _classify (order) :
            """
                Return list to append order to
            """

            if order.event_end.date() < today :
                return past

            elif order.event_start.date() <= today :
                return current

            else :
                return future
        
        for order in orders :
            # resolve list for order
            category = _classify(order)

            category.append(order)

        return past, current, future

    # columns for sorting
    # XXX: sorting not actually implemented...
    COLUMNS = (
        ('order_id',        u"ID",              Order.id),
        ('customer_name',   u"Tilaaja",         Customer.name),
        ('event_name',      u"Tapahtuma",       Order.event_name),
        ('contact_name',    u"Yhteyshenkilö",   Contact.name),
        ('event_start',     u"Ajankohta",       Order.event_start),
    )


    def render_orders_list_rows (self, orders) :
        """
            Render HTML for each of the orders in the order list, inserting sub-captions when the month changes
        """
        
        # XXX: still broken
        from svv import urls
        
        # track each row's year/month
        last_date = None

        # alternating rows
        row_counter = 0

        for order in orders :
            # this order's date - only consider the starting datetime
            date = order.event_start.date()
            
            # month changed?
            if last_date and (date.year, date.month) != (last_date.year, last_date.month) :
                # sub-caption
                yield tags.tr(class_='sub-caption')(
                    tags.th(colspan=len(self.COLUMNS))(
                        date.strftime("%B %Y")
                    )
                )

                # reset
                row_counter = 0
            
            # track
            last_date = date

            # actual row
            yield tags.tr(class_=('alternate' if row_counter % 2 else None))(
                # Id
                tags.td(class_='row-header')(
                    tags.a(href=self.url_for(OrderView, id=order.id))(
                        u"#%d" % (order.id)
                    )
                ),

                # Tilaaja
                tags.td(
                    tags.a(href=self.url_for(urls.CustomerView, id=order.customer.id))(
                        order.customer.name
                    ) if order.customer else "-"
                ),

                # Tapahtuma
                tags.td(
                    tags.a(href=self.url_for(OrderView, id=order.id))(
                        order.event_name
                    )
                ),

                # Yhteyshenkilö
                tags.td(
                    tags.a(href='#')(
                        order.contact.name
                    ) if order.contact else "-"
                ),

                # Ajankohta
                tags.td(
                    order.format_event_time(),
                ),
            )

            row_counter += 1


    def render_orders_list (self, orders, caption) :
        """
            Render HTML for sorted order list
        """
        
        return tags.table(cellspacing="0")(
            tags.caption(caption),
            tags.thead(
                tags.tr(
                    tags.th(
                        title
                    ) for name, title, col in self.COLUMNS
                ),
            ),
            tags.tbody(
                # render using separate loop for sub-captions
                self.render_orders_list_rows(orders)
            )
        )

    def render_content (self) :

        # full order list
        orders_full = self.build_orders_list()

        # categorize
        past, current, future = self.group_orders_by_time(orders_full)

        return (
            tags.h1("Tilauslista"),

            (
                (
                    self.render_orders_list(orders, title),
                ) for title, orders in (
                    (u"Tämänhetkiset", current),
                    (u"Tulevat", future),
                    (u"Menneet", past),
                ) if orders
            ),
        )

class OrderView (PageHandler) :
    """
        Render form for database object, let the user make updates, update the database.
    """
    
    def process (self, id) :
        """
            Set up our object form.

            Process incoming POST data.
        """

        # db session
        self.session = self.app.session()
        
        # order object
        self.order = self.session.query(Order).options(db.eagerload(Order.customer), db.eagerload(Order.contact)).get(id)

    def render_contract_form (self, id) :
        """
            Render the contract panel for our view
        """

        form = OrderContractForm()
        form.defaults()

        # prefilled values?
        if self.POST :
            # feed form POST data
            form.process(self.POST)

        # render, with edit button
        return form.render(action=self.url_for(OrderContractDocument, id=id), edit_button=True)


    def render_order (self, order) :
        """
            Render order fields
        """

        return tags.form(action=self.url_for(EditOrderView, id=order.id), method='GET')(
            tags.fieldset(
                tags.legend(u"Tilaustiedot"),

                
                tags.ol(
                    (tags.li(
                        tags.h3(title),
                        (tags.div(value, class_='value') for value in values if value),

                    ) for title, values in (
                        (u"Tilaaja", (order.customer.name, )),
                        (u"Yhteyshenkilö", (
                            order.contact.name,
                            order.contact.phone,
                            order.contact.email,
                        )),
                        (u"Tapahtuma", (
                            order.event_name,
                            tags.pre(order.event_description.strip(), _whitespace_sensitive=True),
                            order.format_event_time(),
                        )),
                    )),

                    tags.li(
                        tags.input(type='submit', value=u"Muokkaa"),
                    ),
                ),

            ),
        )

    def render_content (self, id) :
        """
            Render our form
        """

        return (
            tags.h1(u"Tilaus #%d" % (id, )),
            tags.h3(u"%s - %s (%s)" % (self.order.customer.name, self.order.event_name, self.order.format_event_time())),
            
            self.render_order(self.order),
            self.render_contract_form(id),
        )


class EditOrderView (PageHandler) :
    """
        Render form for existing order, and update order.

        XXX: concurrent edits!
    """

    def update (self, order, form) :
        """
            Update order values from submitted form data
        """
        
        # modify
        order.customer_id       = form.customer_id
        order.contact_id        = form.contact_id
        order.event_name        = form.event_name
        order.event_description = form.event_description
        order.event_start       = form.event_start
        order.event_end         = form.event_end
        
        # commit
        self.session.commit()

    def process (self, id) :
        """
            Set up our order form.

            Process incoming POST data.
        """

        # db session
        self.session = self.app.session()
        
        # order object
        self.order = self.session.query(Order).options(db.eagerload(Order.customer), db.eagerload(Order.contact)).get(id)

        # form
        self.form = OrderForm(self.app)

        # load object data
        self.form.load(self.order)
        
        # update order?
        if self.POST :

            # feed form our POST data
            if self.form.process(self.POST) :
                # submit data was OK, update order from form
                self.update(self.order, self.form)

                # return to order view
                return self.redirect_for(OrderView, id=self.order.id)

            else :
                # errors, re-show form
                pass
 
    def render_content (self, id) :
        """
            Render our edit form
        """

        return (
            tags.h1(u"Tilaus #%d" % (id, )),

            self.form.render(action=self.url_for(EditOrderView, id=id), return_url=self.url_for(OrderView, id=id)),
        )


class NewOrderView (PageHandler) :
    """
        Render form for input, let the user correct their errors, create the order, and redirect out.
    """

    def create (self, form) :
        """
            Create the new order from the given form data, returning the new order's ID
        """

        # db session
        session = self.app.session()

        # order model
        order = Order()

        # set attrs
        order.customer_id           = form.customer_id
        order.contact_id            = form.contact_id

        order.event_name            = form.event_name
        order.event_description     = form.event_description
        order.event_start           = form.event_start
        order.event_end             = form.event_end

        # add and commit
        session.add(order)
        session.commit()

        # return with new id
        return order.id

    def process (self) :
        """
            Set up up our form.
        """

        self.form = OrderForm(self.app)

        # use either POST data or defaults
        if self.POST :
            # try and process the input, checking for any failures...
            if self.form.process(self.POST) :
                # should be good, create it!
                order_id = self.create(self.form)
                
                # redirect there now that our business is done and the order exists
                return self.redirect_for(OrderView, id=order_id)
            
            else :
                # errors in form input
                pass

        else :
            # init from defaults
            self.form.defaults()
    
    def render_content (self) :
        """
            Render our form
        """

        return (
            tags.h1(u"Uusi tilaus"),
            self.form.render(action=self.url_for(NewOrderView))
        )

class OrderContractDocument (DocumentHandler) :
    """
        Generate and return PDF document for rental contract.
    """

    def process (self, id):
        """
            Return OrderModel object for given ID
        """

        # db session
        self.session = self.app.session()
        
        # order object
        self.order = self.session.query(Order).options(db.eagerload(Order.customer), db.eagerload(Order.contact)).get(id)

        # form
        self.form = OrderContractForm()
        self.form.defaults()
        
        # XXX: also use GET data?
        data = self.request.values

        if self.form.process(data) :
            # render PDF
            pass

        else :
            # show form again

            # XXX: fix the pagelayout shit, this is brutal
            handler = PageHandler(self.app, self.request, self.urlmap)

            # monkey-map
            handler.render_content = self.render_content

            log.warn("Monkeyhack %r -> %r", self, handler)
            
            # XXX: this invokes handler.process(), but...
            return handler.respond(id=id)
    
    def render_content (self, id) :
        """
            Display PDF gen form.
        """
        
        return (
            tags.h1(u"Vuokrasopimus"),
            self.form.render(action=self.url_for(OrderContractDocument, id=id), return_url=self.url_for(OrderView, id=id)),
        )
    
    def generate_document (self, id) :
        """
            Return PDF document to generate
        """

        order = self.order
        params = self.form

        title = "Teekkarispeksi Ry - Vuokrasopimus"
        author = "Teekkarispeksi Ry"

        tpl = pdf.PageTemplate('page',
            header_columns  = (
                ("", ""),
                ("", ""),
                ("", ""),
                ("Vuokrasopimus", [order.customer.name, order.event_name, 'dd.mm.yy hh:mm-hh:mm']),
            ),
            footer_columns  = (
                ("Teekkarispeksi Ry", ("www.teekkarispeksi.fi", )),
                ("Tekniikkavastaava", ("Juha Kallas", "045 xxx yyzz", "jskallas@cc.hut.fi")),
                ("Varastovastaava", ("Joel Pirttimaa", "045 xxx yyzz", "jhpirtti@cc.hut.fi")),
                ("", ""),
            ),
        )

        doc = pdf.DocumentTemplate([tpl],
            title = title, author = author,
        )

        # stylesheet
        styles = pdf.Styles()

        from reportlab.platypus import Paragraph as p
        

        # format
        text = params.contract_text.format(
            order       = order,
        )
        
        # parse to doc tree
        root = markup.Markup().parse(text)

        # format to flowables
        text_elements = list(pdf.Markup(styles).render(root))

        sig_prefill = {}

        if params.prefill_placetime :
            sig_prefill[('Vuokranantaja', 'Aika ja paikka')] = sig_prefill[('Vuokraaja', 'Aika ja paikka')] = params.prefill_placetime

        if params.prefill_ourname :
            sig_prefill[('Vuokranantaja', 'Nimen selvennys')] = params.prefill_ourname

        elements = text_elements + [
                pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), sig_prefill, fullheight=True),

        ]

        # ok
        return doc, elements