Index: payments.py
===================================================================
--- payments.py	(révision 595)
+++ payments.py	(copie de travail)
@@ -6,7 +6,7 @@
 
 from decimal import Decimal
 
-from quixote import redirect, get_request, get_session
+from quixote import redirect, get_publisher, get_request, get_session
 from quixote.directory import Directory
 
 if not set:
@@ -47,9 +47,10 @@
 
 class Invoice(StorableObject):
     _names = 'invoices'
-    _hashed_indexes = ['user_id', 'regie_id']
+    _hashed_indexes = ['user_id', 'user_hash', 'regie_id']
 
     user_id = None
+    user_hash = None
     regie_id = None
     formdef_id = None
     formdata_id = None
@@ -59,6 +60,9 @@
     date = None
     paid = False
     paid_date = None
+    canceled = False
+    canceled_date = None
+    canceled_reason = None
     next_status = None
 
     def __init__(self, id=None, regie_id=None, formdef_id=None):
@@ -90,7 +94,42 @@
             return False
     check_crc = classmethod(check_crc)
 
+    def cancel(self, reason=None):
+        self.canceled = True
+        self.canceled_date = dt.now()
+        if reason:
+            self.canceled_reason = reason
+        self.store()
 
+
+INVOICE_EVO_VIEW = {
+    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
+    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid'),
+    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
+}
+
+class InvoiceEvolutionPart:
+    action = None
+    id = None
+    subject = None
+    amount = None
+
+    def __init__(self, action, invoice):
+        self.action = action
+        self.id = invoice.id
+        self.subject = invoice.subject
+        self.amount = invoice.amount
+
+    def view(self):
+        vars = {
+            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
+            'id': self.id,
+            'subject': self.subject,
+            'amount': self.amount,
+        }
+        return htmltext('<p>' + _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
+
+
 class Transaction(StorableObject):
     _names = 'transactions'
     _hashed_indexes = ['invoice_ids']
@@ -168,7 +207,8 @@
 
     def perform(self, formdata):
         invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
-        invoice.user_id = get_request().user.id  # FIXME: handle user_hash
+        invoice.user_id = formdata.user_id
+        invoice.user_hash = formdata.user_hash
         invoice.formdata_id = formdata.id
         invoice.next_status = self.next_status
         if self.subject:
@@ -181,20 +221,92 @@
         invoice.amount = self.calculate_amount(formdata)
         invoice.date = dt.now()
         invoice.store()
-        # FIXME: add a message in formdata.evolution
-
+        # add a message in formdata.evolution
+        evo = Evolution()
+        evo.time = time.localtime()
+        evo.status = formdata.status
+        evo.add_part(InvoiceEvolutionPart('create', invoice))
+        if not formdata.evolution:
+            formdata.evolution = []
+        formdata.evolution.append(evo)
+        formdata.store()
+        # redirect the user to "my invoices"
         return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
 
 register_item_class(PaymentWorkflowStatusItem)
 
+class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
+    description = N_('Payment Cancel')
+    key = 'payment-cancel'
+    endpoint = False
+
+    reason = None
+    regie_id = None
+
+    def render_as_line(self):
+        if self.regie_id:
+            if self.regie_id == '_all':
+                return _('Cancel all Payments')
+            else:
+                return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
+        else:
+            return _('Cancel Payments (non completed)')
+
+    def get_parameters(self):
+        return ('reason', 'regie_id')
+
+    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
+        if 'reason' in parameters:
+            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
+                     value=self.reason, size=40)
+        if 'regie_id' in parameters:
+            form.add(SingleSelectWidget, '%sregie_id' % prefix,
+                title=_('Regie'), value=self.regie_id,
+                options = [(None, '---'), ('_all', _('All Regies'))] + \
+                            [(x.id, x.label) for x in Regie.select()])
+
+    def perform(self, formdata):
+        invoices_id = []
+        # get all invoices for the formdata and the selected regie
+        for evo in [evo for evo in formdata.evolution if evo.parts]:
+            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
+                if part.action == 'create':
+                    invoices_id.append(part.id)
+                elif part.id in invoices_id:
+                    invoices_id.remove(part.id)
+        invoices = [Invoice.get(id) for id in invoices_id]
+        # select invoices for the selected regie (if not "all regies")
+        if self.regie_id != '_all':
+            invoices = [i for i in invoices if i.regie_id == self.regie_id]
+        # security filter: check user 
+        invoices = [i for i in invoices if (i.user_id == formdata.user_id) \
+                or (i.user_hash == formdata.user_hash)]
+        # security filter: check formdata & formdef
+        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
+                and (i.formdef_id == formdata.formdef.id)]
+        evo = Evolution()
+        evo.time = time.localtime()
+        for invoice in invoices:
+            if not (invoice.paid or invoice.canceled):
+                invoice.cancel(self.reason)
+                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
+        if not formdata.evolution:
+            formdata.evolution = []
+        formdata.evolution.append(evo)
+        formdata.store()
+        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
+
+register_item_class(PaymentCancelWorkflowStatusItem)
+
+
 def request_payment(invoice_ids, url, add_regie=True):
     for invoice_id in invoice_ids:
         if not Invoice.check_crc(invoice_id):
             raise KeyError()
     invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
-    invoices = filter(lambda x: not x.paid, invoices)
+    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
     regie_ids = set([invoice.regie_id for invoice in invoices])
-    # Do not apply if more than one regie is used or no invoice is not paid
+    # Do not apply if more than one regie is used or no invoice is not paid or canceled
     if len(invoices) == 0 or len(regie_ids) != 1:
         url = get_publisher().get_frontoffice_url()
         if get_session().user:
Index: payments_ui.ptl
===================================================================
--- payments_ui.ptl	(révision 595)
+++ payments_ui.ptl	(copie de travail)
@@ -39,6 +39,12 @@
         '<div class="details">'
         htmltext(invoice.details)
         '</div>'
+    if invoice.canceled:
+        '<p class="canceled">'
+        '%s' % _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
+        if invoice.canceled_reason:
+            ' (%s)' % invoice.canceled_reason
+        '</p>'
     if invoice.paid:
         '<p class="paid">%s</p>' % _('paid on %s') % misc.localstrftime(invoice.paid_date)
     '</div>'
@@ -81,7 +87,7 @@
             except KeyError:
                 raise errors.TraversalError()
             invoice_as_html(invoice)
-            if not invoice.paid:
+            if not (invoice.paid or invoice.canceled):
                 regies_id.add(invoice.regie_id)
 
         if len(regies_id) == 1:
Index: myspace.ptl
===================================================================
--- myspace.ptl	(révision 595)
+++ myspace.ptl	(copie de travail)
@@ -35,13 +35,23 @@
         return Directory._q_traverse(self, path)
 
     def _q_index [html] (self):
+        user = get_request().user
+        if not user or user.anonymous:
+            raise errors.AccessUnauthorizedError()
+
         template.html_top(_('Invoices'))
         TextsDirectory.get_html_text('aq-myspace-invoice')
 
         get_session().display_message()
 
-        invoices = Invoice.get_with_indexed_value(
-                        str('user_id'), str(get_request().user.id))
+        invoices = []
+        invoices.extend(Invoice.get_with_indexed_value(
+            str('user_id'), str(user.id)))
+        try:
+            invoices.extend(Invoice.get_with_indexed_value(
+                str('user_hash'), str(user.hash)))
+        except AttributeError:
+            pass
 
         def cmp_invoice(a, b):
             t = cmp(a.regie_id, b.regie_id)
@@ -67,7 +77,7 @@
                 '<ul>'
 
             '<li>'
-            if not invoice.paid:
+            if not (invoice.paid or invoice.canceled):
                 '<input type="checkbox" name="invoice" value="%s"/>' % invoice.id
                 unpaid = True
             misc.localstrftime(invoice.date)
@@ -77,7 +87,11 @@
             '%s' % invoice.amount
             ' &euro;'
             ' - '
-            button = '<strong>%s</strong>' % _('Pay')
+            button = '<span class="paybutton">%s</span>' % _('Pay')
+            if invoice.canceled:
+                _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
+                ' - '
+                button = _('Details')
             if invoice.paid:
                 _('paid on %s') % misc.localstrftime(invoice.paid_date)
                 ' - '
