Copie de photoprint depuis :
authorBenoît Pin <benoit.pin@gmail.com>
Mon, 25 Oct 2010 20:04:49 +0000 (22:04 +0200)
committerBenoît Pin <benoit.pin@gmail.com>
Mon, 25 Oct 2010 20:04:49 +0000 (22:04 +0200)
URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk
Repository Root: http://svn.luxia.fr/svn/labo
Repository UUID: 7eb47c9a-6e02-46bb-968b-2b2bf1974b8d
Revision: 1390
Node Kind: directory
Schedule: normal
Last Changed Author: pin
Last Changed Rev: 1357
Last Changed Date: 2009-09-07 18:06:05 +0200 (Lun, 07 sep 2009)

53 files changed:
__init__.py [new file with mode: 0755]
_utils/import_printing_list.py [new file with mode: 0755]
cart.py [new file with mode: 0644]
configure.zcml [new file with mode: 0644]
exceptions.py [new file with mode: 0755]
graphics/tampon.psd [new file with mode: 0644]
interfaces.py [new file with mode: 0755]
locales/fr/LC_MESSAGES/photoprint.mo [new file with mode: 0644]
locales/fr/LC_MESSAGES/photoprint.po [new file with mode: 0644]
locales/photoprint-manual.pot [new file with mode: 0644]
locales/photoprint.pot [new file with mode: 0644]
order.py [new file with mode: 0755]
permissions.py [new file with mode: 0755]
permissions.zcml [new file with mode: 0644]
price.py [new file with mode: 0755]
skins/confirm_join_template.pt [new file with mode: 0644]
skins/customer_add_control.py [new file with mode: 0755]
skins/customer_join_form.py [new file with mode: 0755]
skins/customer_join_template.pt [new file with mode: 0644]
skins/customer_login_form.pt [new file with mode: 0644]
skins/get_photo_print_order_template.pt [new file with mode: 0644]
skins/my_orders.py [new file with mode: 0755]
skins/my_orders_template.pt [new file with mode: 0644]
skins/order_cancel_form.pt [new file with mode: 0644]
skins/order_list.py [new file with mode: 0755]
skins/order_list_template.pt [new file with mode: 0644]
skins/order_manual_payment_form.pt [new file with mode: 0644]
skins/order_notify_done_form.pt [new file with mode: 0644]
skins/order_notify_sent_form.pt [new file with mode: 0644]
skins/order_payment_template.pt [new file with mode: 0644]
skins/order_printing_list.py [new file with mode: 0755]
skins/order_printing_list_template.pt [new file with mode: 0644]
skins/order_printing_list_template_xml.pt [new file with mode: 0644]
skins/order_states/canceled-en.gif [new file with mode: 0644]
skins/order_states/canceled-fr.gif [new file with mode: 0644]
skins/order_states/done-en.gif [new file with mode: 0644]
skins/order_states/done-fr.gif [new file with mode: 0644]
skins/order_states/paid-en.gif [new file with mode: 0644]
skins/order_states/paid-fr.gif [new file with mode: 0644]
skins/order_states/recorded-en.gif [new file with mode: 0644]
skins/order_states/recorded-fr.gif [new file with mode: 0644]
skins/order_states/refused-en.gif [new file with mode: 0644]
skins/order_states/refused-fr.gif [new file with mode: 0644]
skins/order_states/sent-en.gif [new file with mode: 0644]
skins/order_states/sent-fr.gif [new file with mode: 0644]
skins/order_view.py [new file with mode: 0755]
skins/order_view_template.pt [new file with mode: 0644]
skins/personalize_form.pt [new file with mode: 0755]
skins/photoprint_templates_edit_form.py [new file with mode: 0755]
skins/photoprint_templates_edit_template.pt [new file with mode: 0644]
tool.gif [new file with mode: 0644]
tool.py [new file with mode: 0755]
utils.py [new file with mode: 0755]

diff --git a/__init__.py b/__init__.py
new file mode 100755 (executable)
index 0000000..72d13c0
--- /dev/null
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+############################################################
+# Copyright © 2009 Benoît PIN <pinbe@luxia.fr>             #
+# Cliché - http://luxia.fr                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+"""
+Photo print product. Used to order photo prints.
+
+$Id: __init__.py 1100 2009-06-01 21:48:59Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/__init__.py $
+"""
+from Products.CMFCore import utils as cmfutils
+import tool
+import utils
+import order
+import cart
+import exceptions
+
+
+tools = (tool.PhotoPrintTool,)
+
+def initialize(registrar) :
+       cmfutils.ToolInit('Photoprint Tool',
+                                          tools = tools,
+                                          icon = 'tool.gif'
+                                          ).initialize(registrar)
diff --git a/_utils/import_printing_list.py b/_utils/import_printing_list.py
new file mode 100755 (executable)
index 0000000..5e8e303
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+####################################################
+# Copyright © 2009 Luxia SAS. All rights reserved. #
+#                                                  #
+# Contributors:                                    #
+#  - Benoît Pin <pinbe@luxia.fr>                   #
+####################################################
+"""
+Downloads RSS based order description and make a local human readable file tree
+to facilitate printing tasks.
+
+$Id: import_printing_list.py 1153 2009-06-12 14:07:29Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/_utils/import_printing_list.py $
+"""
+
+
+from urllib2 import HTTPBasicAuthHandler
+from urllib2 import build_opener
+from urllib2 import urlopen
+from xml.dom.minidom import parseString
+from xml.dom import Node
+from os import mkdir, chdir
+from os.path import abspath, join, expanduser, exists
+from getpass import getpass
+
+ELEMENT_NODE = Node.ELEMENT_NODE
+
+def getHttpOpener(url, login, password) :
+       auth_handler = HTTPBasicAuthHandler()
+       host = '/'.join(url.split('/', 3)[:3])
+       auth_handler.add_password('Zope', host, login, password)
+       opener = build_opener(auth_handler)
+       return opener
+
+def getXml(url, opener) :
+       url = '%s?disable_cookie_login__=1' % url
+       xml = opener.open(url).read()
+       return xml
+
+def genFileTree(url, login, password, dest) :
+       opener = getHttpOpener(url, login, password)
+       xml = getXml(url, opener)
+       d = parseString(xml)
+       doc = d.documentElement
+       
+       channel = doc.getElementsByTagName('channel')[0]
+       orderName = getContentOf(channel, 'title')
+
+       chdir(dest)
+       mkdir(orderName)
+       
+       for item in iterElementChildsByTagName(d.documentElement, 'item') :
+               ppTitle = getContentOf(item, 'pp:title')
+               ppQuantity = getContentOf(item, 'pp:quantity')
+
+               printTypePath = join(orderName, ppTitle)
+               printQuantityPath = join(orderName, ppTitle, ppQuantity)
+               
+               if not exists(printTypePath) :
+                       mkdir(printTypePath)
+                       infoFile = open(join(printTypePath, 'info.txt'), 'w')
+                       infoFile.write(getContentOf(item, 'pp:title'))
+                       infoFile.write('\n\n')
+                       infoFile.write(getContentOf(item, 'pp:description'))
+                       infoFile.close()
+               
+               if not exists(printQuantityPath) :
+                       mkdir(printQuantityPath)
+
+               hdUrl = '%s?disable_cookie_login__=1' % getContentOf(item, 'link')
+               localFileName = getContentOf(item, 'title')
+               print localFileName
+               localFile = open(join(printQuantityPath, localFileName), 'w')
+               localFile.write(opener.open(hdUrl).read())
+               localFile.close()
+
+def iterElementChildsByTagName(parent, tagName) :
+       child  = parent.firstChild
+       while child :
+               if child.nodeType == ELEMENT_NODE and child.tagName == tagName :
+                       yield child
+               child = child.nextSibling
+
+def getContentOf(parent, tagName) :
+       child  = parent.firstChild
+       while child :
+               if child.nodeType == ELEMENT_NODE and child.tagName == tagName :
+                       return child.firstChild.nodeValue.encode('utf-8')
+               child = child.nextSibling
+       
+       raise ValueError("%r tag not found" % tagName)
+                       
+
+def main() :
+       url = raw_input('url flux xml de la commande : ')
+       login = raw_input('login : ')
+       password = getpass('mot de passe : ')
+       dest = raw_input('cible [~/Desktop]')
+       if not dest :
+               dest = expanduser('~/Desktop')
+       
+       genFileTree(url, login, password, dest)
+
+if __name__ == '__main__' :
+       main()
+
+
diff --git a/cart.py b/cart.py
new file mode 100644 (file)
index 0000000..fef5a85
--- /dev/null
+++ b/cart.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+####################################################
+# Copyright © 2009 Luxia SAS. All rights reserved. #
+#                                                  #
+# Contributors:                                    #
+#  - Benoît Pin <pinbe@luxia.fr>                   #
+####################################################
+""" Cart definition used to store buyable prints
+
+$Id$
+$URL$
+"""
+from Globals import InitializeClass, Persistent, PersistentMapping
+from Acquisition import Implicit
+from AccessControl import ModuleSecurityInfo
+from Products.CMFCore.utils import getToolByName
+from exceptions import SoldOutError, CartLockedError
+from tool import COPIES_COUNTERS
+from order import CopiesCounters
+
+from logging import getLogger
+console = getLogger('Products.photoprint.cart')
+
+CART_ITEM_KEYS = ['cmf_uid', 'printing_template', 'quantity']
+
+msecurity = ModuleSecurityInfo('Products.photoprint.cart')
+msecurity.declarePublic('PrintCart')
+
+class PrintCart(Persistent, Implicit) :
+       """
+               items are store like that:
+               {<uid>:
+                       {<template>:quantity
+                       ,...}
+               , ...
+               }
+       """
+       
+       __allow_access_to_unprotected_subobjects__ = 1
+       
+       def __init__(self) :
+               self._uids = PersistentMapping()
+               self._order = tuple() # products sequence order
+               self._shippingInfo = PersistentMapping()
+               self._confirmed = False
+               self.pendingOrderPath = ''
+       
+       def setShippingInfo(self, **kw) :
+               self._shippingInfo.update(kw)
+       
+       @property
+       def locked(self):
+               return self._confirmed
+       
+       def append(self, context, item) :
+               if self.locked :
+                       raise CartLockedError
+               assert isinstance(item, dict)
+               keys = item.keys()
+               keys.sort()
+               assert keys == CART_ITEM_KEYS
+
+               pptool = getToolByName(context, 'portal_photo_print')
+               uidh   = getToolByName(context, 'portal_uidhandler')
+               
+               uid = item['cmf_uid']
+               template = item['printing_template']
+               quantity = item['quantity']
+               
+               photo = uidh.getObject(uid)
+               pOptions = pptool.getPrintingOptionsContainerFor(photo)
+               template = getattr(pOptions, template)
+               templateId = template.getId()
+
+               reference = template.productReference
+               
+               # check / update counters
+               if template.maxCopies :
+                       if not hasattr(photo.aq_base, COPIES_COUNTERS) :
+                               setattr(photo, COPIES_COUNTERS, CopiesCounters())
+                       counters = getattr(photo, COPIES_COUNTERS)                      
+                       alreadySold = counters.get(reference)
+                       
+                       if (alreadySold + quantity) > template.maxCopies :
+                               raise SoldOutError(template.maxCopies - alreadySold)
+                       else :
+                               counters[reference] = alreadySold + quantity
+               
+               if not self._uids.has_key(uid) :
+                       self._uids[uid] = PersistentMapping()
+                       self._order = self._order + (uid,)
+               
+               if not self._uids[uid].has_key(templateId) :
+                       self._uids[uid][templateId] = PersistentMapping()
+                       self._uids[uid][templateId]['reference'] = reference
+                       self._uids[uid][templateId]['quantity'] = 0
+
+               self._uids[uid][templateId]['quantity'] += quantity
+       
+       def update(self, context, item) :
+               if self.locked :
+                       raise CartLockedError
+               assert isinstance(item, dict)
+               keys = item.keys()
+               keys.sort()
+               assert keys == CART_ITEM_KEYS
+
+               pptool = getToolByName(context, 'portal_photo_print')
+               uidh   = getToolByName(context, 'portal_uidhandler')
+               
+               uid = item['cmf_uid']
+               template = item['printing_template']
+               quantity = item['quantity']
+               
+               photo = uidh.getObject(uid)
+               pOptions = pptool.getPrintingOptionsContainerFor(photo)
+               template = getattr(pOptions, template)
+               templateId = template.getId()
+               reference = template.productReference
+
+               currentQuantity = self._uids[uid][templateId]['quantity']
+               delta = quantity - currentQuantity
+               if template.maxCopies :
+                       counters = getattr(photo, COPIES_COUNTERS)
+                       if delta > 0 :
+                               already = counters[reference]
+                               if (already + delta) > template.maxCopies :
+                                       raise SoldOutError(template.maxCopies - already)
+                       counters[reference] += delta
+
+               self._uids[uid][templateId]['quantity'] += delta
+       
+       def remove(self, context, uid, templateId) :
+               if self.locked :
+                       raise CartLockedError
+               pptool = getToolByName(context, 'portal_photo_print')
+               uidh   = getToolByName(context, 'portal_uidhandler')
+                               
+               photo = uidh.getObject(uid)
+               pOptions = pptool.getPrintingOptionsContainerFor(photo)
+               template = getattr(pOptions, templateId)
+               reference = template.productReference
+               
+               quantity = self._uids[uid][templateId]['quantity']
+               if template.maxCopies :
+                       counters = getattr(photo, COPIES_COUNTERS)
+                       counters[reference] -= quantity
+               
+               del self._uids[uid][templateId]
+               if not self._uids[uid] :
+                       del self._uids[uid]
+                       self._order = tuple([u for u in self._order if u != uid])
+               
+       
+       def __iter__(self) :
+               for uid in self._order :
+                       item = {}
+                       item['cmf_uid'] = uid
+                       for templateId, rq in self._uids[uid].items() :
+                               item['printing_template'] = templateId
+                               item['quantity'] = rq['quantity']
+                               yield item
+       
+       def __nonzero__(self) :
+               return len(self._order) > 0
+
diff --git a/configure.zcml b/configure.zcml
new file mode 100644 (file)
index 0000000..5bd5a30
--- /dev/null
@@ -0,0 +1,37 @@
+<!--
+$Id: configure.zcml 650 2009-02-04 14:28:17Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/configure.zcml $
+-->
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta"
+    xmlns:cmf="http://namespaces.zope.org/cmf"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:i18n="http://namespaces.zope.org/i18n">
+
+  <include file="permissions.zcml"/>
+  <cmf:registerDirectory directory="skins" name="photoprint" recursive="True"/>
+  <i18n:registerTranslations directory="locales"/>
+
+  <five:registerClass
+      class=".order.PrintOrderTemplate"
+      meta_type="Print order template"
+      permission="photoprint.AddPrintOrderTemplate"
+      />
+  <utility
+      component=".order.PrintOrderTemplateFactory"
+      name="photoprint.order_template"
+      />
+  
+  <meta:redefinePermission from="zope2.Public" to="zope.Public" />
+  <five:registerClass
+      class=".order.PrintOrder"
+      meta_type="Print order"
+      permission="photoprint.AddPrintOrder"
+      />
+
+  <utility
+      component=".order.PrintOrderFactory"
+      name="photoprint.order"
+      />
+</configure>
diff --git a/exceptions.py b/exceptions.py
new file mode 100755 (executable)
index 0000000..e045fbb
--- /dev/null
@@ -0,0 +1,21 @@
+""" photoprint exceptions
+
+$Id: exceptions.py 805 2009-03-19 23:59:45Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/exceptions.py $
+"""
+from AccessControl import ModuleSecurityInfo
+
+security = ModuleSecurityInfo('Products.photoprint.exceptions')
+
+security.declarePublic('SoldOutError')
+class SoldOutError(Exception):
+       "Item is sold out"
+       
+       __allow_access_to_unprotected_subobjects__ = 1
+       
+       def __init__(self, n=0):
+               self.n = n
+
+security.declarePublic('CartLockedError')
+class CartLockedError(Exception) :
+       "Operation is not permitted due to cart lock"
\ No newline at end of file
diff --git a/graphics/tampon.psd b/graphics/tampon.psd
new file mode 100644 (file)
index 0000000..f85261e
Binary files /dev/null and b/graphics/tampon.psd differ
diff --git a/interfaces.py b/interfaces.py
new file mode 100755 (executable)
index 0000000..47113f0
--- /dev/null
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+############################################################
+# Copyright © 2009 Benoît PIN <pinbe@luxia.fr>             #
+# Cliché - http://luxia.fr                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+"""
+Printable objects interfaces
+
+$Id: interfaces.py 707 2009-02-26 15:25:49Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/interfaces.py $
+"""
+
+from zope.interface import Interface, Attribute
+
+class IPrintingSupport(Interface) :
+       """
+       Used to describe physical printing support
+       """
+       
+       title = Attribute("Support title")
+       description = Attribute("support description - HTML format")
+       manufacturer = Attribute("Manufacturer name")
+       surfaceWeight = Attribute("Surface weight. SI unit (kg/m**2)")
+       category = Attribute("Category of support (glossy, mate...)")
+       availableSize = Attribute("Paper sheet or roll paper available for this support")
+       
+
+class IPrintableSheet(Interface) :
+       """
+       Printable sheet description
+       """
+       
+       width = Attribute("Physical support width")
+       height = Attribute("Physical support height")
+       formatName = Attribute("Format name if exists")
+       deviceMargins = Attribute("Mapping of margins indexed by device model.")
+       
+       sizeUnits = Attribute("Measurement units for all sizing attributes")
+       
+       price = Attribute("Public price of the printed sheet")
+       manufacturerReference = Attribute("Manufacturer reference")
+       
+       
+class IPrintableRoll(Interface):
+       """
+       Printable roll description
+       """
+       width = Attribute("Roll width")
+       maxLength = Attribute("Roll length")
+       formatName = Attribute("Format name if exists")
+       deviceMargins = Attribute("Mapping of margins (top and bottom) indexed by device model.")
+       
+       sizeUnits = Attribute("Measurement units for all sizing attributes")
+       
+       pricePerLength = Attribute("Public price of the printed support per length unit (ie. €/m)")
+       manufacturerReference = Attribute("Manufacturer reference")
+
+class IPrintOrderTemplate(Interface):
+       """
+       predefined print order, suggested by the seller.
+       """
+       
+       title = Attribute("order name")
+       description = Attribute("order description")
+       
+       price = Attribute("Public price of the order")
+
+class IPrintOrder(Interface) :
+       """
+       the general purpose print order
+       """
diff --git a/locales/fr/LC_MESSAGES/photoprint.mo b/locales/fr/LC_MESSAGES/photoprint.mo
new file mode 100644 (file)
index 0000000..6aa0836
Binary files /dev/null and b/locales/fr/LC_MESSAGES/photoprint.mo differ
diff --git a/locales/fr/LC_MESSAGES/photoprint.po b/locales/fr/LC_MESSAGES/photoprint.po
new file mode 100644 (file)
index 0000000..63f0e86
--- /dev/null
@@ -0,0 +1,574 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Plinn 2.0\n"
+"POT-Creation-Date: $Date: Mon Sep  7 17:56:32 2009 $\n"
+"PO-Revision-Date: 2009-09-07 17:58+0200\n"
+"Last-Translator:  Benoît PIN\n"
+"Language-Team: CRI http://cri.ensmp.fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"Language-Code: fr\n"
+"Language-Name: French\n"
+"Preferred-Encodings: utf-8 latin1\n"
+"Domain: plinn\n"
+"X-Is-Fallback-For: fr-fr, fr-be fr-ca fr-lu fr-mc fr-ch\n"
+
+#. Default: ""
+#: price.py:72
+msgid "${i}.${d}"
+msgstr "${i},${d}"
+
+#: skins/confirm_join_template.pt:12
+msgid "You have been registered as a member."
+msgstr "Vous venez d'être enregistré en tant que membre."
+
+#: skins/confirm_join_template.pt:14
+msgid ""
+"You will receive an email shortly containing your password and instructions "
+"on how to activate your membership."
+msgstr ""
+"Vous recevrez sous peu un email comportant votre mot de passe avec des "
+"instructions pour activer votre compte."
+
+#: skins/confirm_join_template.pt:19
+msgid "Click the button to log in immediately."
+msgstr "Cliquez sur le bouton pour vous connecter immédiatement."
+
+#: skins/confirm_join_template.pt:26
+msgid "Log in"
+msgstr "Connexion"
+
+#: skins/customer_add_control.py:19
+msgid "Please enter a given name."
+msgstr "Veuillez entrer un prénom."
+
+#: skins/customer_add_control.py:20
+msgid "Please enter a name."
+msgstr "Veuillez entrer un nom."
+
+#: skins/customer_add_control.py:21
+msgid "Please enter an email."
+msgstr "Veuillez entrer un email."
+
+#: skins/customer_add_control.py:22
+msgid "Please enter a member id."
+msgstr "Veuillez entrer un login."
+
+#: skins/customer_add_control.py:23
+msgid "Please enter a billing address."
+msgstr "Veuillez entrer une addresse de facturation."
+
+#: skins/customer_add_control.py:24
+msgid "Please enter a city."
+msgstr "Veuillez entrer une ville."
+
+#: skins/customer_add_control.py:25
+msgid "Please enter zip code."
+msgstr "Veuillez entrer un code postal."
+
+#: skins/customer_add_control.py:26
+msgid "Please enter a country."
+msgstr "Veuillez entrer un pays."
+
+#: skins/customer_add_control.py:27
+msgid "Please enter a phone."
+msgstr "Veuillez entrer un numéro de téléphone."
+
+#: skins/customer_add_control.py:28
+msgid "Please accept general conditions of sales."
+msgstr "Veuillez accepter les conditions générales des ventes."
+
+#: skins/my_orders.py:17 skins/order_list.py:14
+#: skins/order_view_template.pt:190
+msgid "Date"
+msgstr "Date"
+
+#: skins/my_orders.py:21 skins/order_list.py:22
+#: skins/photoprint_templates_edit_template.pt:42
+msgid "Reference"
+msgstr "Référence"
+
+#: skins/my_orders.py:25 skins/order_list.py:26
+msgid "Prints"
+msgstr "Tirages"
+
+#: skins/my_orders.py:29 skins/order_list.py:30
+msgid "Amount"
+msgstr "Montant"
+
+#: skins/my_orders.py:33 skins/order_list.py:34
+msgid "State"
+msgstr "État"
+
+#: skins/my_orders.py:69
+msgid "My orders"
+msgstr "Mes commandes"
+
+#: skins/my_orders_template.pt:34 skins/order_cancel_form.pt:19
+#: skins/order_list_template.pt:33 skins/order_list_template.pt:35
+#: skins/order_list_template.pt:37 skins/order_list_template.pt:50
+#: skins/order_manual_payment_form.pt:19 skins/order_notify_done_form.pt:19
+#: skins/order_notify_sent_form.pt:19 skins/order_view_template.pt:87
+#: skins/order_view_template.pt:201
+msgid "${DYNAMIC_CONTENT}"
+msgstr ""
+
+#: skins/order_cancel_form.pt:11
+msgid "Cancel order \"${order_reference}\""
+msgstr "Annuler la commande « ${order_reference} »"
+
+#: skins/order_cancel_form.pt:12
+msgid "Cancel the order and relist reserved copies."
+msgstr "Annuler la commande et remettre en vente les exemplaires réservées."
+
+#: skins/order_cancel_form.pt:18 skins/order_manual_payment_form.pt:18
+#: skins/order_notify_done_form.pt:18 skins/order_notify_sent_form.pt:18
+msgid "Current state:"
+msgstr "État actuel :"
+
+#. Default: "Subject"
+#: skins/order_cancel_form.pt:23 skins/order_notify_sent_form.pt:24
+msgid "mail_subject"
+msgstr "Objet"
+
+#: skins/order_cancel_form.pt:31 skins/order_notify_done_form.pt:21
+#: skins/order_notify_sent_form.pt:32
+msgid "Comments"
+msgstr "Commentaires"
+
+#: skins/order_cancel_form.pt:39
+msgid ""
+"Due to a lack of payment since ${creation_date}, your order has been "
+"canceled. The ${store_name} team."
+msgstr ""
+"Suite à une absence de paiement depuis le ${creation_date}, votre commande a "
+"été annulée.\n"
+"\n"
+"L'équipe ${store_name}."
+
+#: skins/order_cancel_form.pt:46 skins/order_notify_sent_form.pt:59
+msgid "Send email"
+msgstr "Envoyer un email"
+
+#: skins/order_cancel_form.pt:60
+msgid "Cancel the order"
+msgstr "Annuler la commande"
+
+#: skins/order_list.py:18
+msgid "Customer"
+msgstr "Client"
+
+#: skins/order_list.py:46
+msgid "descending sort"
+msgstr "tri décroissant"
+
+#: skins/order_list.py:49
+msgid "ascending sort"
+msgstr "tri croissant"
+
+#: skins/order_list_template.pt:17 skins/order_view_template.pt:31
+#: skins/order_view_template.pt:56 skins/personalize_form.pt:33
+msgid "Name"
+msgstr "Nom"
+
+#: skins/order_manual_payment_form.pt:11
+msgid "Pay order \"${order_reference}\" manually."
+msgstr "Payer la commande « ${order_reference} » manuellement."
+
+#: skins/order_manual_payment_form.pt:12
+msgid "Pay manually"
+msgstr "Payer manuellement"
+
+#: skins/order_manual_payment_form.pt:21
+msgid "Payment description"
+msgstr "Description du paiement"
+
+#: skins/order_manual_payment_form.pt:24
+msgid "Please indicate payment mean and references"
+msgstr "Veuillez indiquer le moyen de paiement et les références."
+
+#: skins/order_manual_payment_form.pt:29
+msgid "Pay"
+msgstr "Payer"
+
+#: skins/order_notify_done_form.pt:11
+msgid "Notify order \"${order_reference}\" done."
+msgstr "Notifier la commande « ${order_reference} » effectuée."
+
+#: skins/order_notify_done_form.pt:12
+msgid "Notify that order has been made."
+msgstr "Notifier que cette commande vient d'être effectuée."
+
+#: skins/order_notify_done_form.pt:26 skins/order_notify_sent_form.pt:73
+msgid "Notify"
+msgstr "Notifier"
+
+#: skins/order_notify_sent_form.pt:11
+msgid "Notify order \"${order_reference}\" sent."
+msgstr "Notifier l'expédition de la commande « ${order_reference} »"
+
+#: skins/order_notify_sent_form.pt:12
+msgid "Notify that order has been sent."
+msgstr "Notifier que cette commande vient d'être expédiée."
+
+#: skins/order_notify_sent_form.pt:21
+msgid "Tracking info"
+msgstr "Informations de suivi"
+
+#: skins/order_notify_sent_form.pt:39
+msgid ""
+"Your order has been sent. You will receive your prints soon. You will be "
+"able to track your parcel with the informations below. The ${store_name} "
+"team thanks you for your confidence and wish you receipt you order."
+msgstr ""
+"Votre commande vient d'être expédiée, vous la recevrez prochainement. Vous "
+"pourrez suivre son acheminement à l'aide des informations ci-dessous.\n"
+"\n"
+"L'équipe ${store_name} vous remercie de votre confiance et vous souhaite "
+"bonne réception de votre commande."
+
+#: skins/order_notify_sent_form.pt:47
+msgid "Tracking number"
+msgstr "Numéro de suivi"
+
+#: skins/order_notify_sent_form.pt:53
+msgid "Tracking url"
+msgstr "url de suivi"
+
+#: skins/order_printing_list_template.pt:22 skins/order_view_template.pt:97
+msgid "Image"
+msgstr "Image"
+
+#: skins/order_printing_list_template.pt:23
+msgid "File"
+msgstr "Fichier"
+
+#: skins/order_printing_list_template.pt:24
+msgid "Format / type"
+msgstr "Format / type"
+
+#: skins/order_printing_list_template.pt:25 skins/order_view_template.pt:99
+msgid "Quantity"
+msgstr "Quantité"
+
+#: skins/order_view_template.pt:100
+msgid "Unit price"
+msgstr "Prix unitaire"
+
+#: skins/order_view_template.pt:101
+#: skins/photoprint_templates_edit_template.pt:45
+#: skins/photoprint_templates_edit_template.pt:131
+msgid "VAT (%)"
+msgstr "TVA (%)"
+
+#: skins/order_view_template.pt:102
+msgid "Amount<br />(tax incl.)"
+msgstr "Montant (TTC)"
+
+#: skins/order_view_template.pt:111
+msgid "image removed"
+msgstr "image supprimée"
+
+#: skins/order_view_template.pt:138
+msgid "VAT"
+msgstr "TVA"
+
+#: skins/order_view_template.pt:142
+msgid "Total amount to pay"
+msgstr "Montant total à payer"
+
+#: skins/order_view_template.pt:154
+msgid "Use one of these button to pay:"
+msgstr "Utilisez un de ces bouton pour payer :"
+
+#: skins/order_view_template.pt:167
+msgid ""
+"Please click over the button representing your credit card. You will leave "
+"temporarily this web site to pay your order on our bank partner payment "
+"site. After your payment, you will be able to come back to the store and get "
+"an invoice of your transaction."
+msgstr ""
+"Veuillez cliquer sur le bouton correspondant à votre carte banquaire. Vous "
+"allez quitter monmentanément ce site pour payer sur le site de notre banque "
+"partenaire. Après votre paiement, vous pourrez revenir à la boutique et "
+"obtenir une facture de votre transaction."
+
+#: skins/order_view_template.pt:174
+msgid ""
+"This secured payment is provided by the Cyberplus™ payment service of "
+"\"Banque Populaire\"."
+msgstr ""
+"Le paiement sécurisé est assuré par le service Cyberplus™ de la « Banque "
+"Populaire »."
+
+#: skins/order_view_template.pt:185
+msgid "Order processing history"
+msgstr "Historique du traitement de la commande"
+
+#: skins/order_view_template.pt:191
+msgid "Actor"
+msgstr "Acteur"
+
+#: skins/order_view_template.pt:192
+msgid "Action"
+msgstr "Action"
+
+#: skins/order_view_template.pt:23
+msgid "Order Nb. ${order_number}"
+msgstr "Commande N° ${order_number}"
+
+#: skins/order_view_template.pt:28
+msgid "Billing"
+msgstr "Facturation"
+
+#: skins/order_view_template.pt:35 skins/order_view_template.pt:60
+#: skins/personalize_form.pt:50
+msgid "Address"
+msgstr "Adresse"
+
+#: skins/order_view_template.pt:39 skins/order_view_template.pt:64
+#: skins/personalize_form.pt:57
+msgid "City"
+msgstr "Ville"
+
+#: skins/order_view_template.pt:43 skins/order_view_template.pt:68
+#: skins/personalize_form.pt:63
+msgid "Zip code"
+msgstr "Code postal"
+
+#: skins/order_view_template.pt:47 skins/order_view_template.pt:72
+#: skins/personalize_form.pt:69
+msgid "Country"
+msgstr "Pays"
+
+#: skins/order_view_template.pt:53 skins/order_view_template.pt:134
+msgid "Shipping"
+msgstr "Livraison"
+
+#: skins/order_view_template.pt:84
+msgid "Processing"
+msgstr "Traitement"
+
+#: skins/order_view_template.pt:98
+msgid "Printing format and type"
+msgstr "Format d'impression et type de support"
+
+#: skins/personalize_form.pt:21
+msgid "Member Preferences"
+msgstr "Préférences utilisateur"
+
+#: skins/personalize_form.pt:23
+msgid "${link} to change your password."
+msgstr "${link} pour changer votre mot de passe."
+
+#: skins/personalize_form.pt:23
+msgid "Click here"
+msgstr "Cliquer ici"
+
+#: skins/personalize_form.pt:27
+msgid "Given Name"
+msgstr "Prénom"
+
+#: skins/personalize_form.pt:39
+msgid "Email address"
+msgstr "Adresse e-mail"
+
+#: skins/personalize_form.pt:46
+msgid "Billing informations"
+msgstr "Informations de facturation"
+
+#: skins/personalize_form.pt:79
+msgid "Phone"
+msgstr "Téléphone"
+
+#: skins/personalize_form.pt:88
+msgid "Change"
+msgstr "Modifier"
+
+#: skins/photoprint_templates_edit_form.py:56
+msgid "Printing options added."
+msgstr "Options d'impression ajoutées."
+
+#: skins/photoprint_templates_edit_form.py:60
+msgid "Printing options deleted."
+msgstr "Options d'impression supprimées."
+
+#: skins/photoprint_templates_edit_template.pt:106
+msgid "Description"
+msgstr "Description"
+
+#: skins/photoprint_templates_edit_template.pt:112
+msgid "Product reference"
+msgstr "Référence produit"
+
+#. Default: "The 0 value means unlimited"
+#: skins/photoprint_templates_edit_template.pt:122
+msgid "max_copies_field_help"
+msgstr "La valeur 0 signifie illimité"
+
+#: skins/photoprint_templates_edit_template.pt:140
+msgid "Add"
+msgstr "Ajouter"
+
+#: skins/photoprint_templates_edit_template.pt:142
+msgid "Cancel"
+msgstr "Annuler"
+
+#: skins/photoprint_templates_edit_template.pt:150
+msgid "Save"
+msgstr "Enregistrer"
+
+#: skins/photoprint_templates_edit_template.pt:19
+msgid "No printing options are defined at this level."
+msgstr "Aucune option d'impression définie à ce niveau."
+
+#: skins/photoprint_templates_edit_template.pt:23
+msgid "The printing options that apply here are defined above:"
+msgstr ""
+"Les options d'impressions qui s'appliquent ici sont définies plus haut :"
+
+#: skins/photoprint_templates_edit_template.pt:28
+msgid "Define printing options"
+msgstr "Définir des options d'impression"
+
+#: skins/photoprint_templates_edit_template.pt:41
+#: skins/photoprint_templates_edit_template.pt:100
+msgid "Title"
+msgstr "Titre"
+
+#: skins/photoprint_templates_edit_template.pt:43
+#: skins/photoprint_templates_edit_template.pt:118
+msgid "Max. number of copies"
+msgstr "Nombre maxi d'exemplaires"
+
+#: skins/photoprint_templates_edit_template.pt:44
+#: skins/photoprint_templates_edit_template.pt:127
+msgid "Price"
+msgstr "Prix"
+
+#: skins/photoprint_templates_edit_template.pt:80
+#: skins/photoprint_templates_edit_template.pt:81
+msgid "Add print order template"
+msgstr "Ajouter un modèle d'ordre d'impression"
+
+#: skins/photoprint_templates_edit_template.pt:91
+msgid "Delete options defined at this level"
+msgstr "Supprimer les options définies à ce niveau"
+
+#: tool.py:109
+msgid "No printing options found at %r"
+msgstr "Aucune option d'impression trouvée sur %r"
+
+#: tool.py:193
+msgid "You must enter a title."
+msgstr "Vous devez saisir un titre."
+
+#: tool.py:197
+msgid ""
+"You must enter an integer number\n"
+"for the maximum number of copies."
+msgstr ""
+"Vous devez saisir un nombre entier\n"
+"pour le nombre maxi de copies."
+
+#: tool.py:199
+msgid ""
+"You must enter a positive value\n"
+"for the maximum number of copies."
+msgstr ""
+"Vous devez saisir une valeur positive\n"
+"pour le nombre maxi de copies."
+
+#: tool.py:203
+msgid "You must enter a numeric value for the price."
+msgstr "Vous devez saisir une valeur numérique pour le prix."
+
+#: tool.py:208
+msgid "You must enter a numeric value for the VAT rate."
+msgstr "Vous devez saisir une valeur numérique pour la TVA (pourcentage)."
+
+msgid "Your payment is complete."
+msgstr "Votre paiement a été accepté."
+
+msgid "Your payment has been canceled. You will be able to pay later."
+msgstr "Votre paiement a été annulé. Vous pourrez payer plus tard."
+
+msgid "Your payment has been refused."
+msgstr "Votre paiement a été refusé."
+
+msgid "recorded-en.gif"
+msgstr "recorded-fr.gif"
+
+msgid "done-en.gif"
+msgstr "done-fr.gif"
+
+msgid "paid-en.gif"
+msgstr "paid-fr.gif"
+
+msgid "refused-en.gif"
+msgstr "refused-fr.gif"
+
+msgid "sent-en.gif"
+msgstr "sent-fr.gif"
+
+msgid "canceled-en.gif"
+msgstr "canceled-fr.gif"
+
+msgid "auto_accept_payment"
+msgstr "Paiement accepté"
+
+msgid "auto_cancel_order"
+msgstr "Paiement annulé"
+
+msgid "auto_refuse_payment"
+msgstr "Paiement refusé"
+
+msgid "auto_transaction_failed"
+msgstr "Transaction échouée"
+
+msgid "manual_payment"
+msgstr "Paiement manuel"
+
+msgid "notify_done"
+msgstr "Commande effectuée"
+
+msgid "notify_sent"
+msgstr "Commande expédiée"
+
+msgid "canceled"
+msgstr "annulée"
+
+msgid "done"
+msgstr "effectuée"
+
+msgid "paid"
+msgstr "payée"
+
+msgid "recorded"
+msgstr "enregistrée"
+
+msgid "refused"
+msgstr "refusée"
+
+msgid "sent"
+msgstr "expédiée"
+
+msgid "Manual payment"
+msgstr "Paiement manuel"
+
+msgid "Notify done"
+msgstr "Notifier comme fait"
+
+msgid "Notify sent"
+msgstr "Notifier l'expédition"
+
+msgid "Printing list"
+msgstr "Liste d'impression"
+
+msgid "[%s] order %s canceling notification"
+msgstr "[%s] annulation de la commande %s"
+
+msgid "[%s] order %s sending notification"
+msgstr "[%s] expédition de la commande %s"
diff --git a/locales/photoprint-manual.pot b/locales/photoprint-manual.pot
new file mode 100644 (file)
index 0000000..688317d
--- /dev/null
@@ -0,0 +1,85 @@
+# from photoprint-manual.pot
+
+msgid "Your payment is complete."
+msgstr ""
+
+msgid "Your payment has been canceled. You will be able to pay later."
+msgstr ""
+
+msgid "Your payment has been refused."
+msgstr ""
+
+msgid "recorded-en.gif"
+msgstr ""
+
+msgid "done-en.gif"
+msgstr ""
+
+msgid "paid-en.gif"
+msgstr ""
+
+msgid "refused-en.gif"
+msgstr ""
+
+msgid "sent-en.gif"
+msgstr ""
+
+msgid "canceled-en.gif"
+msgstr ""
+
+msgid "auto_accept_payment"
+msgstr ""
+
+msgid "auto_cancel_order"
+msgstr ""
+
+msgid "auto_refuse_payment"
+msgstr ""
+
+msgid "auto_transaction_failed"
+msgstr ""
+
+msgid "manual_payment"
+msgstr ""
+
+msgid "notify_done"
+msgstr ""
+
+msgid "notify_sent"
+msgstr ""
+
+msgid "canceled"
+msgstr ""
+
+msgid "done"
+msgstr ""
+
+msgid "paid"
+msgstr ""
+
+msgid "recorded"
+msgstr ""
+
+msgid "refused"
+msgstr ""
+
+msgid "sent"
+msgstr ""
+
+msgid "Manual payment"
+msgstr ""
+
+msgid "Notify done"
+msgstr ""
+
+msgid "Notify sent"
+msgstr ""
+
+msgid "Printing list"
+msgstr ""
+
+msgid "[%s] order %s canceling notification"
+msgstr ""
+
+msgid "[%s] order %s sending notification"
+msgstr ""
diff --git a/locales/photoprint.pot b/locales/photoprint.pot
new file mode 100644 (file)
index 0000000..476e0b2
--- /dev/null
@@ -0,0 +1,567 @@
+############################################################
+# Copyright © 2005-2008  Benoît PIN <benoit.pin@ensmp.fr>  #
+# Plinn - http://plinn.org                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: Plinn - Portfolio 2\n"
+"POT-Creation-Date: $Date: Mon Sep  7 17:56:32 2009 $\n"
+"Language-Team: Benoît Pin <benoit.pin@ensmp.fr>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: price.py:72
+#. Default: ""
+msgid "${i}.${d}"
+msgstr ""
+
+#: skins/confirm_join_template.pt:12
+msgid "You have been registered as a member."
+msgstr ""
+
+#: skins/confirm_join_template.pt:14
+msgid "You will receive an email shortly containing your password and instructions on how to activate your membership."
+msgstr ""
+
+#: skins/confirm_join_template.pt:19
+msgid "Click the button to log in immediately."
+msgstr ""
+
+#: skins/confirm_join_template.pt:26
+msgid "Log in"
+msgstr ""
+
+#: skins/customer_add_control.py:19
+msgid "Please enter a given name."
+msgstr ""
+
+#: skins/customer_add_control.py:20
+msgid "Please enter a name."
+msgstr ""
+
+#: skins/customer_add_control.py:21
+msgid "Please enter an email."
+msgstr ""
+
+#: skins/customer_add_control.py:22
+msgid "Please enter a member id."
+msgstr ""
+
+#: skins/customer_add_control.py:23
+msgid "Please enter a billing address."
+msgstr ""
+
+#: skins/customer_add_control.py:24
+msgid "Please enter a city."
+msgstr ""
+
+#: skins/customer_add_control.py:25
+msgid "Please enter zip code."
+msgstr ""
+
+#: skins/customer_add_control.py:26
+msgid "Please enter a country."
+msgstr ""
+
+#: skins/customer_add_control.py:27
+msgid "Please enter a phone."
+msgstr ""
+
+#: skins/customer_add_control.py:28
+msgid "Please accept general conditions of sales."
+msgstr ""
+
+#: skins/my_orders.py:17
+#: skins/order_list.py:14
+#: skins/order_view_template.pt:190
+msgid "Date"
+msgstr ""
+
+#: skins/my_orders.py:21
+#: skins/order_list.py:22
+#: skins/photoprint_templates_edit_template.pt:42
+msgid "Reference"
+msgstr ""
+
+#: skins/my_orders.py:25
+#: skins/order_list.py:26
+msgid "Prints"
+msgstr ""
+
+#: skins/my_orders.py:29
+#: skins/order_list.py:30
+msgid "Amount"
+msgstr ""
+
+#: skins/my_orders.py:33
+#: skins/order_list.py:34
+msgid "State"
+msgstr ""
+
+#: skins/my_orders.py:69
+msgid "My orders"
+msgstr ""
+
+#: skins/my_orders_template.pt:34
+#: skins/order_cancel_form.pt:19
+#: skins/order_list_template.pt:33
+#: skins/order_list_template.pt:35
+#: skins/order_list_template.pt:37
+#: skins/order_list_template.pt:50
+#: skins/order_manual_payment_form.pt:19
+#: skins/order_notify_done_form.pt:19
+#: skins/order_notify_sent_form.pt:19
+#: skins/order_view_template.pt:87
+#: skins/order_view_template.pt:201
+msgid "${DYNAMIC_CONTENT}"
+msgstr ""
+
+#: skins/order_cancel_form.pt:11
+msgid "Cancel order \"${order_reference}\""
+msgstr ""
+
+#: skins/order_cancel_form.pt:12
+msgid "Cancel the order and relist reserved copies."
+msgstr ""
+
+#: skins/order_cancel_form.pt:18
+#: skins/order_manual_payment_form.pt:18
+#: skins/order_notify_done_form.pt:18
+#: skins/order_notify_sent_form.pt:18
+msgid "Current state:"
+msgstr ""
+
+#: skins/order_cancel_form.pt:23
+#: skins/order_notify_sent_form.pt:24
+#. Default: "Subject"
+msgid "mail_subject"
+msgstr ""
+
+#: skins/order_cancel_form.pt:31
+#: skins/order_notify_done_form.pt:21
+#: skins/order_notify_sent_form.pt:32
+msgid "Comments"
+msgstr ""
+
+#: skins/order_cancel_form.pt:39
+msgid "Due to a lack of payment since ${creation_date}, your order has been canceled. The ${store_name} team."
+msgstr ""
+
+#: skins/order_cancel_form.pt:46
+#: skins/order_notify_sent_form.pt:59
+msgid "Send email"
+msgstr ""
+
+#: skins/order_cancel_form.pt:60
+msgid "Cancel the order"
+msgstr ""
+
+#: skins/order_list.py:18
+msgid "Customer"
+msgstr ""
+
+#: skins/order_list.py:46
+msgid "descending sort"
+msgstr ""
+
+#: skins/order_list.py:49
+msgid "ascending sort"
+msgstr ""
+
+#: skins/order_list_template.pt:17
+#: skins/order_view_template.pt:31
+#: skins/order_view_template.pt:56
+#: skins/personalize_form.pt:33
+msgid "Name"
+msgstr ""
+
+#: skins/order_manual_payment_form.pt:11
+msgid "Pay order \"${order_reference}\" manually."
+msgstr ""
+
+#: skins/order_manual_payment_form.pt:12
+msgid "Pay manually"
+msgstr ""
+
+#: skins/order_manual_payment_form.pt:21
+msgid "Payment description"
+msgstr ""
+
+#: skins/order_manual_payment_form.pt:24
+msgid "Please indicate payment mean and references"
+msgstr ""
+
+#: skins/order_manual_payment_form.pt:29
+msgid "Pay"
+msgstr ""
+
+#: skins/order_notify_done_form.pt:11
+msgid "Notify order \"${order_reference}\" done."
+msgstr ""
+
+#: skins/order_notify_done_form.pt:12
+msgid "Notify that order has been made."
+msgstr ""
+
+#: skins/order_notify_done_form.pt:26
+#: skins/order_notify_sent_form.pt:73
+msgid "Notify"
+msgstr ""
+
+#: skins/order_notify_sent_form.pt:11
+msgid "Notify order \"${order_reference}\" sent."
+msgstr ""
+
+#: skins/order_notify_sent_form.pt:12
+msgid "Notify that order has been sent."
+msgstr ""
+
+#: skins/order_notify_sent_form.pt:21
+msgid "Tracking info"
+msgstr ""
+
+#: skins/order_notify_sent_form.pt:39
+msgid "Your order has been sent. You will receive your prints soon. You will be able to track your parcel with the informations below. The ${store_name} team thanks you for your confidence and wish you receipt you order."
+msgstr ""
+
+#: skins/order_notify_sent_form.pt:47
+msgid "Tracking number"
+msgstr ""
+
+#: skins/order_notify_sent_form.pt:53
+msgid "Tracking url"
+msgstr ""
+
+#: skins/order_printing_list_template.pt:22
+#: skins/order_view_template.pt:97
+msgid "Image"
+msgstr ""
+
+#: skins/order_printing_list_template.pt:23
+msgid "File"
+msgstr ""
+
+#: skins/order_printing_list_template.pt:24
+msgid "Format / type"
+msgstr ""
+
+#: skins/order_printing_list_template.pt:25
+#: skins/order_view_template.pt:99
+msgid "Quantity"
+msgstr ""
+
+#: skins/order_view_template.pt:100
+msgid "Unit price"
+msgstr ""
+
+#: skins/order_view_template.pt:101
+#: skins/photoprint_templates_edit_template.pt:45
+#: skins/photoprint_templates_edit_template.pt:131
+msgid "VAT (%)"
+msgstr ""
+
+#: skins/order_view_template.pt:102
+msgid "Amount<br />(tax incl.)"
+msgstr ""
+
+#: skins/order_view_template.pt:111
+msgid "image removed"
+msgstr ""
+
+#: skins/order_view_template.pt:138
+msgid "VAT"
+msgstr ""
+
+#: skins/order_view_template.pt:142
+msgid "Total amount to pay"
+msgstr ""
+
+#: skins/order_view_template.pt:154
+msgid "Use one of these button to pay:"
+msgstr ""
+
+#: skins/order_view_template.pt:167
+msgid "Please click over the button representing your credit card. You will leave temporarily this web site to pay your order on our bank partner payment site. After your payment, you will be able to come back to the store and get an invoice of your transaction."
+msgstr ""
+
+#: skins/order_view_template.pt:174
+msgid "This secured payment is provided by the Cyberplus\342\204\242 payment service of \"Banque Populaire\"."
+msgstr ""
+
+#: skins/order_view_template.pt:185
+msgid "Order processing history"
+msgstr ""
+
+#: skins/order_view_template.pt:191
+msgid "Actor"
+msgstr ""
+
+#: skins/order_view_template.pt:192
+msgid "Action"
+msgstr ""
+
+#: skins/order_view_template.pt:23
+msgid "Order Nb. ${order_number}"
+msgstr ""
+
+#: skins/order_view_template.pt:28
+msgid "Billing"
+msgstr ""
+
+#: skins/order_view_template.pt:35
+#: skins/order_view_template.pt:60
+#: skins/personalize_form.pt:50
+msgid "Address"
+msgstr ""
+
+#: skins/order_view_template.pt:39
+#: skins/order_view_template.pt:64
+#: skins/personalize_form.pt:57
+msgid "City"
+msgstr ""
+
+#: skins/order_view_template.pt:43
+#: skins/order_view_template.pt:68
+#: skins/personalize_form.pt:63
+msgid "Zip code"
+msgstr ""
+
+#: skins/order_view_template.pt:47
+#: skins/order_view_template.pt:72
+#: skins/personalize_form.pt:69
+msgid "Country"
+msgstr ""
+
+#: skins/order_view_template.pt:53
+#: skins/order_view_template.pt:134
+msgid "Shipping"
+msgstr ""
+
+#: skins/order_view_template.pt:84
+msgid "Processing"
+msgstr ""
+
+#: skins/order_view_template.pt:98
+msgid "Printing format and type"
+msgstr ""
+
+#: skins/personalize_form.pt:21
+msgid "Member Preferences"
+msgstr ""
+
+#: skins/personalize_form.pt:23
+msgid "${link} to change your password."
+msgstr ""
+
+#: skins/personalize_form.pt:23
+msgid "Click here"
+msgstr ""
+
+#: skins/personalize_form.pt:27
+msgid "Given Name"
+msgstr ""
+
+#: skins/personalize_form.pt:39
+msgid "Email address"
+msgstr ""
+
+#: skins/personalize_form.pt:46
+msgid "Billing informations"
+msgstr ""
+
+#: skins/personalize_form.pt:79
+msgid "Phone"
+msgstr ""
+
+#: skins/personalize_form.pt:88
+msgid "Change"
+msgstr ""
+
+#: skins/photoprint_templates_edit_form.py:56
+msgid "Printing options added."
+msgstr ""
+
+#: skins/photoprint_templates_edit_form.py:60
+msgid "Printing options deleted."
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:106
+msgid "Description"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:112
+msgid "Product reference"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:122
+#. Default: "The 0 value means unlimited"
+msgid "max_copies_field_help"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:140
+msgid "Add"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:142
+msgid "Cancel"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:150
+msgid "Save"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:19
+msgid "No printing options are defined at this level."
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:23
+msgid "The printing options that apply here are defined above:"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:28
+msgid "Define printing options"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:41
+#: skins/photoprint_templates_edit_template.pt:100
+msgid "Title"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:43
+#: skins/photoprint_templates_edit_template.pt:118
+msgid "Max. number of copies"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:44
+#: skins/photoprint_templates_edit_template.pt:127
+msgid "Price"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:80
+#: skins/photoprint_templates_edit_template.pt:81
+msgid "Add print order template"
+msgstr ""
+
+#: skins/photoprint_templates_edit_template.pt:91
+msgid "Delete options defined at this level"
+msgstr ""
+
+#: tool.py:109
+msgid "No printing options found at %r"
+msgstr ""
+
+#: tool.py:193
+msgid "You must enter a title."
+msgstr ""
+
+#: tool.py:197
+msgid ""
+"You must enter an integer number\n"
+"for the maximum number of copies."
+msgstr ""
+
+#: tool.py:199
+msgid ""
+"You must enter a positive value\n"
+"for the maximum number of copies."
+msgstr ""
+
+#: tool.py:203
+msgid "You must enter a numeric value for the price."
+msgstr ""
+
+#: tool.py:208
+msgid "You must enter a numeric value for the VAT rate."
+msgstr ""
+
+# from photoprint-manual.pot
+
+msgid "Your payment is complete."
+msgstr ""
+
+msgid "Your payment has been canceled. You will be able to pay later."
+msgstr ""
+
+msgid "Your payment has been refused."
+msgstr ""
+
+msgid "recorded-en.gif"
+msgstr ""
+
+msgid "done-en.gif"
+msgstr ""
+
+msgid "paid-en.gif"
+msgstr ""
+
+msgid "refused-en.gif"
+msgstr ""
+
+msgid "sent-en.gif"
+msgstr ""
+
+msgid "canceled-en.gif"
+msgstr ""
+
+msgid "auto_accept_payment"
+msgstr ""
+
+msgid "auto_cancel_order"
+msgstr ""
+
+msgid "auto_refuse_payment"
+msgstr ""
+
+msgid "auto_transaction_failed"
+msgstr ""
+
+msgid "manual_payment"
+msgstr ""
+
+msgid "notify_done"
+msgstr ""
+
+msgid "notify_sent"
+msgstr ""
+
+msgid "canceled"
+msgstr ""
+
+msgid "done"
+msgstr ""
+
+msgid "paid"
+msgstr ""
+
+msgid "recorded"
+msgstr ""
+
+msgid "refused"
+msgstr ""
+
+msgid "sent"
+msgstr ""
+
+msgid "Manual payment"
+msgstr ""
+
+msgid "Notify done"
+msgstr ""
+
+msgid "Notify sent"
+msgstr ""
+
+msgid "Printing list"
+msgstr ""
+
+msgid "[%s] order %s canceling notification"
+msgstr ""
+
+msgid "[%s] order %s sending notification"
+msgstr ""
diff --git a/order.py b/order.py
new file mode 100755 (executable)
index 0000000..c166eb9
--- /dev/null
+++ b/order.py
@@ -0,0 +1,517 @@
+# -*- coding: utf-8 -*-
+############################################################
+# Copyright © 2009 Benoît PIN <pinbe@luxia.fr>             #
+# Cliché - http://luxia.fr                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+"""
+Print order classes
+
+$Id: order.py 1357 2009-09-07 16:06:05Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/order.py $
+"""
+
+from Globals import InitializeClass, PersistentMapping, Persistent
+from Acquisition import Implicit
+from AccessControl import ClassSecurityInfo
+from AccessControl.requestmethod import postonly
+from zope.interface import implements
+from zope.component.factory import Factory
+from OFS.SimpleItem import SimpleItem
+from ZTUtils import make_query
+from Products.CMFCore.PortalContent import PortalContent
+from Products.CMFCore.permissions import ModifyPortalContent, View
+from Products.CMFCore.utils import getToolByName, getUtilityByInterfaceName
+from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
+from Products.Plinn.utils import getPreferredLanguages
+from interfaces import IPrintOrderTemplate, IPrintOrder
+from permissions import ManagePrintOrderTemplate, ManagePrintOrders
+from price import Price
+from utils import Message as _
+from utils import translate
+from xml.dom.minidom import Document
+from tool import COPIES_COUNTERS
+from App.config import getConfiguration
+try :
+       from Products.cyberplus import CyberplusConfig
+       from Products.cyberplus import CyberplusRequester
+       from Products.cyberplus import CyberplusResponder
+       from Products.cyberplus import LANGUAGE_VALUES as CYBERPLUS_LANGUAGES
+except ImportError:
+       pass
+from logging import getLogger
+console = getLogger('Products.photoprint.order')
+
+
+def _getCyberplusConfig() :
+       zopeConf = getConfiguration()
+       try :
+               conf = zopeConf.product_config['cyberplus']
+       except KeyError :
+               EnvironmentError("No cyberplus configuration found in Zope environment.")
+       
+       merchant_id = conf['merchant_id']
+       bin_path = conf['bin_path']
+       path_file = conf['path_file']
+       merchant_country = conf['merchant_country']
+       
+       config = CyberplusConfig(merchant_id,
+                                                        bin_path,
+                                                        path_file,
+                                                        merchant_country=merchant_country)
+       return config
+
+
+class PrintOrderTemplate(SimpleItem) :
+       """
+       predefined print order
+       """
+       implements(IPrintOrderTemplate)
+       
+       security = ClassSecurityInfo()
+       
+       def __init__(self
+                               , id
+                               , title=''
+                               , description=''
+                               , productReference=''
+                               , maxCopies=0
+                               , price=0
+                               , VATRate=0) :
+               self.id = id
+               self.title = title
+               self.description = description
+               self.productReference = productReference
+               self.maxCopies = maxCopies # 0 means unlimited
+               self.price = Price(price, VATRate)
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'edit')
+       def edit( self
+                       , title=''
+                       , description=''
+                       , productReference=''
+                       , maxCopies=0
+                       , price=0
+                       , VATRate=0 ) :
+               self.title = title
+               self.description = description
+               self.productReference = productReference
+               self.maxCopies = maxCopies
+               self.price = Price(price, VATRate)
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'formWidgetData')
+       def formWidgetData(self, REQUEST=None, RESPONSE=None):
+               """formWidgetData documentation
+               """
+               d = Document()
+               d.encoding = 'utf-8'
+               root = d.createElement('formdata')
+               d.appendChild(root)
+               
+               def gua(name) :
+                       return str(getattr(self, name, '')).decode('utf-8')
+               
+               id = d.createElement('id')
+               id.appendChild(d.createTextNode(self.getId()))
+               root.appendChild(id)
+               
+               title = d.createElement('title')
+               title.appendChild(d.createTextNode(gua('title')))
+               root.appendChild(title)
+               
+               description = d.createElement('description')
+               description.appendChild(d.createTextNode(gua('description')))
+               root.appendChild(description)
+               
+               productReference = d.createElement('productReference')
+               productReference.appendChild(d.createTextNode(gua('productReference')))
+               root.appendChild(productReference)
+               
+               maxCopies = d.createElement('maxCopies')
+               maxCopies.appendChild(d.createTextNode(str(self.maxCopies)))
+               root.appendChild(maxCopies)
+               
+               price = d.createElement('price')
+               price.appendChild(d.createTextNode(str(self.price.taxed)))
+               root.appendChild(price)
+               
+               vatrate = d.createElement('VATRate')
+               vatrate.appendChild(d.createTextNode(str(self.price.vat)))
+               root.appendChild(vatrate)
+
+               if RESPONSE is not None :
+                       RESPONSE.setHeader('content-type', 'text/xml; charset=utf-8')
+                       
+                       manager = getToolByName(self, 'caching_policy_manager', None)
+                       if manager is not None:
+                               view_name = 'formWidgetData'
+                               headers = manager.getHTTPCachingHeaders(
+                                                                 self, view_name, {}
+                                                                 )
+                               
+                               for key, value in headers:
+                                       if key == 'ETag':
+                                               RESPONSE.setHeader(key, value, literal=1)
+                                       else:
+                                               RESPONSE.setHeader(key, value)
+                               if headers:
+                                       RESPONSE.setHeader('X-Cache-Headers-Set-By',
+                                                                          'CachingPolicyManager: %s' %
+                                                                          '/'.join(manager.getPhysicalPath()))
+               
+               
+               return d.toxml('utf-8')
+
+               
+InitializeClass(PrintOrderTemplate)
+PrintOrderTemplateFactory = Factory(PrintOrderTemplate)
+
+class PrintOrder(PortalContent, DefaultDublinCoreImpl) :
+       
+       implements(IPrintOrder)
+       security = ClassSecurityInfo()
+       
+       def __init__( self, id) :
+               DefaultDublinCoreImpl.__init__(self)
+               self.id = id
+               self.items = []
+               self.quantity = 0
+               self.price = Price(0, 0)
+               # billing and shipping addresses
+               self.billing = PersistentMapping()
+               self.shipping = PersistentMapping()
+               self.shippingFees = Price(0,0)
+               self._paymentResponse = PersistentMapping()
+       
+       @property
+       def amountWithFees(self) :
+               return self.price + self.shippingFees
+       
+       
+       security.declareProtected(ModifyPortalContent, 'editBilling')
+       def editBilling(self
+                                       , name
+                                       , address
+                                       , city
+                                       , zipcode
+                                       , country
+                                       , phone) :
+               self.billing['name'] = name
+               self.billing['address'] = address
+               self.billing['city'] = city
+               self.billing['zipcode'] = zipcode
+               self.billing['country'] = country
+               self.billing['phone'] = phone
+       
+       security.declareProtected(ModifyPortalContent, 'editShipping')
+       def editShipping(self, name, address, city, zipcode, country) :
+               self.shipping['name'] = name
+               self.shipping['address'] = address
+               self.shipping['city'] = city
+               self.shipping['zipcode'] = zipcode
+               self.shipping['country'] = country
+       
+       security.declarePrivate('loadCart')
+       def loadCart(self, cart):
+               pptool = getToolByName(self, 'portal_photo_print')
+               uidh = getToolByName(self, 'portal_uidhandler')
+               mtool = getToolByName(self, 'portal_membership')
+               
+               items = []
+               for item in cart :
+                       photo = uidh.getObject(item['cmf_uid'])
+                       pOptions = pptool.getPrintingOptionsContainerFor(photo)
+                       template = getattr(pOptions, item['printing_template'])
+
+                       reference = template.productReference
+                       quantity = item['quantity']
+                       uPrice = template.price
+                       self.quantity += quantity
+               
+                       d = {'cmf_uid'                  : item['cmf_uid']
+                               ,'url'                          : photo.absolute_url()
+                               ,'title'                        : template.title
+                               ,'description'          : template.description
+                               ,'unit_price'           : Price(uPrice._taxed, uPrice._rate)
+                               ,'quantity'                     : quantity
+                               ,'productReference'     : reference
+                               }
+                       items.append(d)
+                       self.price += uPrice * quantity
+                       # confirm counters
+                       if template.maxCopies :
+                               counters = getattr(photo, COPIES_COUNTERS)
+                               counters.confirm(reference, quantity)
+                               
+               self.items = tuple(items)
+
+               member = mtool.getAuthenticatedMember()
+               mg = lambda name : member.getProperty(name, '')
+               billing = {'name'               : member.getMemberFullName(nameBefore=0)
+                                 ,'address'    : mg('billing_address')
+                                 ,'city'               : mg('billing_city')
+                                 ,'zipcode'    : mg('billing_zipcode')
+                                 ,'country'    : mg('country')
+                                 ,'phone'              : mg('phone') }
+               self.editBilling(**billing)
+               
+               sg = lambda name : cart._shippingInfo.get(name, '')
+               shipping = {'name'              : sg('shipping_fullname')
+                                  ,'address'   : sg('shipping_address')
+                                  ,'city'              : sg('shipping_city')
+                                  ,'zipcode'   : sg('shipping_zipcode')
+                                  ,'country'   : sg('shipping_country')}
+               self.editShipping(**shipping)
+               
+               self.shippingFees = pptool.getShippingFeesFor(shippable=self)
+               
+               cart._confirmed = True
+               cart.pendingOrderPath = self.getPhysicalPath()
+       
+       security.declareProtected(ManagePrintOrders, 'resetCopiesCounters')
+       def resetCopiesCounters(self) :
+               pptool = getToolByName(self, 'portal_photo_print')
+               uidh = getToolByName(self, 'portal_uidhandler')
+               
+               for item in self.items :
+                       photo = uidh.getObject(item['cmf_uid'])
+                       counters = getattr(photo, COPIES_COUNTERS, None)
+                       if counters :
+                               counters.cancel(item['productReference'],
+                                                               item['quantity'])
+       
+       security.declareProtected(View, 'getPaymentRequest')
+       def getPaymentRequest(self) :
+               config = _getCyberplusConfig()
+               requester = CyberplusRequester(config)
+               hereurl = self.absolute_url()
+               amount = self.price + self.shippingFees
+               amount = amount.getValues()['taxed']
+               amount = amount * 100
+               amount = str(int(round(amount, 0)))
+               pptool = getToolByName(self, 'portal_photo_print')
+               transaction_id = pptool.getNextTransactionId()
+               
+               userLanguages = getPreferredLanguages(self)
+               for pref in userLanguages :
+                       lang = pref.split('-')[0]
+                       if lang in CYBERPLUS_LANGUAGES :
+                               break
+               else :
+                       lang = 'en'
+               
+               options = {  'amount': amount
+                                       ,'cancel_return_url'            : '%s/paymentCancelHandler' % hereurl
+                                       ,'normal_return_url'            : '%s/paymentManualResponseHandler' % hereurl
+                                       ,'automatic_response_url'       :'%s/paymentAutoResponseHandler' % hereurl
+                                       ,'transaction_id'                       : transaction_id
+                                       ,'order_id'                             : self.getId()
+                                       ,'language'                                     : lang
+                                  }
+               req = requester.generateRequest(options)
+               return req
+       
+       def _decodeCyberplusResponse(self, form) :
+               config = _getCyberplusConfig()
+               responder = CyberplusResponder(config)
+               response = responder.getResponse(form)
+               return response
+       
+       def _compareWithAutoResponse(self, manu) :
+               keys = manu.keys()
+               auto = self._paymentResponse
+               autoKeys = auto.keys()
+               if len(keys) != len(autoKeys) :
+                       console.warn('Manual has not the same keys.\nauto: %r\nmanual: %r' % \
+                               (sorted(autoKeys), sorted(keys)))
+               else :
+                       for k, v in manu.items() :
+                               if not auto.has_key(k) :
+                                       console.warn('%r field only found in manual response.' % k)
+                               else :
+                                       if v != auto[k] :
+                                               console.warn('data mismatch for %r\nauto: %r\nmanual: %r' % (k, auto[k], v))
+       
+       def _checkOrderId(self, response) :
+               expected = self.getId()
+               assert expected == response['order_id'], \
+                       "Cyberplus response transaction_id doesn't match the order object:\n" \
+                       "expected: %s\n" \
+                       "found: %s" % (expected, response['transaction_id'])
+       
+       def _executeOrderWfTransition(self, response) :
+               if CyberplusResponder.transactionAccepted(response) :
+                       wfaction = 'auto_accept_payment'
+               elif CyberplusResponder.transactionRefused(response) :
+                       self.resetCopiesCounters()
+                       wfaction = 'auto_refuse_payment'
+               elif CyberplusResponder.transactionCanceled(response) :
+                       wfaction = 'auto_cancel_order'
+               else :
+                       # transaction failed
+                       wfaction = 'auto_transaction_failed'
+
+               wtool = getToolByName(self, 'portal_workflow')
+               wf = wtool.getWorkflowById('order_workflow')
+               tdef = wf.transitions.get(wfaction)
+               wf._changeStateOf(self, tdef)
+               wtool._reindexWorkflowVariables(self)
+       
+       security.declarePublic('paymentAutoResponseHandler')
+       @postonly
+       def paymentAutoResponseHandler(self, REQUEST) :
+               """\
+               Handle cyberplus payment auto response.
+               """
+               response = self._decodeCyberplusResponse(REQUEST.form)
+               self._checkOrderId(response)
+               self._paymentResponse.update(response)
+               self._executeOrderWfTransition(response)
+       
+       @postonly
+       def paymentManualResponseHandler(self, REQUEST) :
+               """\
+               Handle cyberplus payment manual response.
+               """
+               response = self._decodeCyberplusResponse(REQUEST.form)
+               self._checkOrderId(response)
+               
+               autoResponse = self._paymentResponse
+               if not autoResponse :
+                       console.warn('Manual response handled before auto response at %s' % '/'.join(self.getPhysicalPath()))
+                       self._paymentResponse.update(response)
+                       self._executeOrderWfTransition(response)
+               else :
+                       self._compareWithAutoResponse(response)
+                       
+               url = '%s?%s' % (self.absolute_url(),
+                                               make_query(portal_status_message=translate('Your payment is complete.', self).encode('utf-8'))
+                                               )
+               return REQUEST.RESPONSE.redirect(url)
+       
+       @postonly
+       def paymentCancelHandler(self, REQUEST) :
+               """\
+               Handle cyberplus cancel response.
+               This handler can be invoqued in two cases:
+               - the user cancel the payment form
+               - the payment transaction has been refused
+               """
+               response = self._decodeCyberplusResponse(REQUEST.form)
+               self._checkOrderId(response)
+               
+               if self._paymentResponse :
+                       # normaly, it happens when the transaction is refused by cyberplus.
+                       self._compareWithAutoResponse(response)
+
+               
+               if CyberplusResponder.transactionRefused(response) :
+                       if not self._paymentResponse :
+                               console.warn('Manual response handled before auto response at %s' % '/'.join(self.getPhysicalPath()))
+                               self._paymentResponse.update(response)
+                               self._executeOrderWfTransition(response)
+                       
+                       msg = 'Your payment has been refused.'
+
+               else :
+                       self._executeOrderWfTransition(response)
+                       msg = 'Your payment has been canceled. You will be able to pay later.'
+
+               url = '%s?%s' % (self.absolute_url(),
+                                               make_query(portal_status_message= \
+                                               translate(msg, self).encode('utf-8'))
+                                               )
+               return REQUEST.RESPONSE.redirect(url)
+               
+       
+       def getCustomerSummary(self) :
+               ' '
+               return {'quantity':self.quantity,
+                               'price':self.price}
+                       
+       
+InitializeClass(PrintOrder)
+PrintOrderFactory = Factory(PrintOrder)
+
+
+class CopiesCounters(Persistent, Implicit) :
+
+       def __init__(self):
+               self._mapping = PersistentMapping()
+       
+       def getBrowserId(self):
+               sdm = self.session_data_manager
+               bim = sdm.getBrowserIdManager()
+               browserId = bim.getBrowserId(create=1)
+               return browserId
+       
+       def _checkBrowserId(self, browserId) :
+               sdm = self.session_data_manager
+               sd = sdm.getSessionDataByKey(browserId)
+               return not not sd
+       
+       def __setitem__(self, reference, count) :
+               if not self._mapping.has_key(reference):
+                       self._mapping[reference] = PersistentMapping()
+                       self._mapping[reference]['pending'] = PersistentMapping()
+                       self._mapping[reference]['confirmed'] = 0
+               
+               globalCount = self[reference]
+               delta = count - globalCount
+               bid = self.getBrowserId()
+               if not self._mapping[reference]['pending'].has_key(bid) :
+                       self._mapping[reference]['pending'][bid] = delta
+               else :
+                       self._mapping[reference]['pending'][bid] += delta
+               
+       
+       def __getitem__(self, reference) :
+               item = self._mapping[reference]
+               globalCount = item['confirmed']
+               
+               for browserId, count in item['pending'].items() :
+                       if self._checkBrowserId(browserId) :
+                               globalCount += count
+                       else :
+                               del self._mapping[reference]['pending'][browserId]
+
+               return globalCount
+       
+       def get(self, reference, default=0) :
+               if self._mapping.has_key(reference) :
+                       return self[reference]
+               else :
+                       return default
+       
+       def getPendingCounter(self, reference) :
+               bid = self.getBrowserId()
+               if not self._checkBrowserId(bid) :
+                       console.warn('BrowserId not found: %s' % bid)
+                       return 0
+
+               count = self._mapping[reference]['pending'].get(bid, None)
+               if count is None :
+                       console.warn('No pending data found for browserId %s' % bid)
+                       return 0
+               else :
+                       return count
+       
+       def confirm(self, reference, quantity) :
+               pending = self.getPendingCounter(reference)
+               if pending != quantity :
+                       console.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending, quantity))
+
+               browserId = self.getBrowserId()
+               if self._mapping[reference]['pending'].has_key(browserId) :
+                       del self._mapping[reference]['pending'][browserId]
+               self._mapping[reference]['confirmed'] += quantity
+       
+       def cancel(self, reference, quantity) :
+               self._mapping[reference]['confirmed'] -= quantity
+       
+       def __str__(self):
+               return str(self._mapping)
diff --git a/permissions.py b/permissions.py
new file mode 100755 (executable)
index 0000000..5d6da42
--- /dev/null
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+############################################################
+# Copyright © 2009 Benoît PIN <pinbe@luxia.fr>             #
+# Cliché - http://luxia.fr                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+"""
+photoprint specific permissions
+
+$Id: permissions.py 1121 2009-06-08 15:41:55Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/permissions.py $
+"""
+
+from AccessControl import ModuleSecurityInfo
+from Products.CMFCore.permissions import setDefaultRoles
+
+security = ModuleSecurityInfo('Products.photoprint.permissions')
+
+ManagePrintOrderTemplate = "Manage print order template"
+security.declarePublic('ManagePrintOrderTemplate')
+setDefaultRoles(ManagePrintOrderTemplate, ('Manager',))
+
+AddPrintOrder =  "Add print order"
+security.declarePublic('AddPrintOrder')
+setDefaultRoles(AddPrintOrder, ('Authenticated', 'Manager',))
+
+ListPrintOrders = "List print orders"
+security.declarePublic('ListPrintOrders')
+setDefaultRoles(ListPrintOrders, ('Manager',))
+
+ManagePrintOrders = "Manage print orders"
+security.declarePublic('ManagePrintOrders')
+setDefaultRoles(ManagePrintOrders, ('Manager',))
diff --git a/permissions.zcml b/permissions.zcml
new file mode 100644 (file)
index 0000000..98206f8
--- /dev/null
@@ -0,0 +1,18 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+  <permission
+    id="photoprint.AddPrintOrderTemplate"
+    title="Add print order template"
+    />
+  <permission
+    id="photoprint.AddPrintOrder"
+    title="Add print order"
+    />
+  <permission
+    id="photoprint.ListPrintOrders"
+    title="List print orders"
+    />
+       <permission
+               id="photoprint.ManagePrintOrders"
+               title="Manage print orders"
+               />
+</configure>
diff --git a/price.py b/price.py
new file mode 100755 (executable)
index 0000000..f04e063
--- /dev/null
+++ b/price.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+############################################################
+# Copyright © 2009 Benoît PIN <pinbe@luxia.fr>             #
+# Cliché - http://luxia.fr                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+"""
+Pricing types
+
+$Id: price.py 834 2009-03-28 15:11:54Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/price.py $
+"""
+
+from Globals import Persistent
+from AccessControl import ModuleSecurityInfo
+from utils import Message as _
+from utils import translate
+from Products.globalrequest import getRequest
+
+msecurity = ModuleSecurityInfo('Products.photoprint.price')
+msecurity.declarePublic('Price')
+
+class Price(object, Persistent) :
+       """
+       Price of an object which have VAT tax.
+       """
+       __allow_access_to_unprotected_subobjects__ = 1
+       
+       def __init__(self, taxedPrice, rate=0) :
+               """price is initialized with taxed value"""
+               self._rate = float(rate)
+               self._setTaxed(taxedPrice)
+
+       @property
+       def rate(self):
+               return self._localeStrNum(self._rate)
+       
+       def _setTaxed(self, value) :
+               self._taxed = value
+               self._price = value / (1 + self._rate)
+       
+       @property
+       def taxed(self) :
+               return self._localeStrNum(self._taxed)
+       
+       @property
+       def value(self) :
+               return self._localeStrNum(self._price)
+       
+       @property
+       def tax(self) :
+               tax = self._rate * self._price
+               return self._localeStrNum(tax)
+       
+       @property
+       def vat(self) :
+               "returns vat rate in percent"
+               vat = self._rate * 100
+               return self._localeStrNum(vat)
+       
+       def _localeStrNum(self, n) :
+               i = int(n)
+               if i == n :
+                       return str(i)
+               else :
+                       n = str(round(n, 2))
+                       i, d = n.split('.')
+                       ds = _(u'${i}.${d}', mapping={'i':i, 'd':d}, default=n)
+                       return  translate(ds, getRequest()).encode('utf-8')
+       
+       def getValues(self) :
+               values = {'value':self._price,
+                                 'taxed': self._taxed,
+                                 'rate':self._rate}
+               return values
+               
+       
+       def __add__(self, other) :
+               taxed = self._taxed + other._taxed
+               value = self._price + other._price
+               rate = (taxed - value ) / float(value)
+               return Price(taxed, rate)
+       
+       def __div__(self, other) :
+               return Price(self._taxed / other, self._rate)
+       
+       def __mul__(self, other) :
+               return Price(self._taxed * other, self._rate)
+       
+       def __repr__(self):
+               return '%s with VAT' % self.taxed
diff --git a/skins/confirm_join_template.pt b/skins/confirm_join_template.pt
new file mode 100644 (file)
index 0000000..688147d
--- /dev/null
@@ -0,0 +1,31 @@
+<html metal:use-macro="here/main_template/macros/master"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  <head>
+    <title>titre</title>
+    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
+    
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs" tal:omit-tag="">
+       <p i18n:translate="">You have been registered as a member.</p>
+
+       <p tal:condition="options/validate_email" i18n:translate="">You will receive
+         an email shortly containing your password and instructions on how to
+         activate your membership.</p>
+
+       <tal:case tal:condition="not: options/validate_email">
+               <p i18n:translate="">Click the button to log in immediately.</p>
+               <form tal:attributes="action options/form_action">
+                       <input type="hidden" name="__ac_name"  value=""
+                              tal:attributes="value options/member_id" />
+                       <input type="hidden" name="__ac_password" value=""
+                              tal:attributes="value options/password" />
+                       <input type="hidden" name="noAjax" value="1" />
+                       <input type="submit" name="login" value="Log in" i18n:attributes="value"/>
+               </form>
+       </tal:case>
+    </div>
+  </body>
+</html>
diff --git a/skins/customer_add_control.py b/skins/customer_add_control.py
new file mode 100755 (executable)
index 0000000..274f4c6
--- /dev/null
@@ -0,0 +1,57 @@
+##parameters=**kw
+from Products.CMFCore.utils import getToolByName
+from Products.realis.utils import translate
+from Products.CMFDefault.utils import translate as cmf_translate
+rtool = getToolByName(context, 'portal_registration')
+ptool = getToolByName(context, 'portal_properties')
+_ = lambda msg : translate(msg, context)
+
+kg = lambda name : kw.get(name, '').strip()
+
+cmfprops = {'username' : kg('member_id')
+                  ,'email'             : kg('member_email')}
+
+failMessage = rtool.testPropertiesValidity(cmfprops)
+if failMessage is not None :
+       return context.setStatus(False, cmf_translate(failMessage, context))
+
+mandatoryFields = [
+         ('given_name', _('Please enter a given name.'))
+       , ('name', _('Please enter a name.'))
+       , ('member_email', _('Please enter an email.'))
+       , ('member_id', _('Please enter a member id.'))
+       , ('billing_address', _('Please enter a billing address.'))
+       , ('billing_city', _('Please enter a city.'))
+       , ('billing_zipcode', _('Please enter zip code.'))
+       , ('country', _('Please enter a country.'))
+       , ('phone', _('Please enter a phone.'))
+       , ('accept_gcs', _('Please accept general conditions of sales.'))
+       ]
+
+for name, failMessage in mandatoryFields :
+       value = kg(name)
+       if not value :
+               return context.setStatus(False, failMessage)
+
+
+try:
+       rtool.addMember( id=kg('member_id'),
+                                        password=kg('password'),
+                                        properties={'username'                 : kg('member_id')
+                                                               ,'given_name'           : kg('given_name')
+                                                               ,'name'                         : kg('name')
+                                                               ,'email'                        : kg('member_email')
+                                                               ,'billing_address'      : kg('billing_address')
+                                                               ,'billing_city'     : kg('billing_city')
+                                                               ,'billing_zipcode'      : kg('billing_zipcode')
+                                                               ,'country'                      : kg('country')
+                                                               ,'phone'                        : kg('phone')
+                                                               ,'accept_gcs'           : kg('accep_gcs')} )
+except ValueError, errmsg:
+       return context.setStatus(False, _(errmsg))
+
+
+if kg('send_password') or ptool.getProperty('validate_email') :
+       rtool.registeredNotify(kg('member_id'))
+
+return context.setStatus(True, 'Success!')
diff --git a/skins/customer_join_form.py b/skins/customer_join_form.py
new file mode 100755 (executable)
index 0000000..4df2848
--- /dev/null
@@ -0,0 +1,57 @@
+##parameters=add=''
+from Products.CMFCore.utils import getToolByName
+from Products.realis.utils import translate
+from ZTUtils import make_query as mq
+_ = lambda msg : translate(msg, context)
+ptool = getToolByName(script, 'portal_properties')
+atool = getToolByName(script, 'portal_actions')
+validate_email = ptool.getProperty('validate_email')
+options = {}
+options['validate_email'] = validate_email
+
+req = context.REQUEST
+resp = req.RESPONSE
+form = req.form
+fg = lambda name : form.get(name,'').strip()
+
+
+if add and \
+       context.validatePassword(**form) and \
+       context.customer_add_control(**form) :
+       came_from = fg('came_from')
+       if came_from :
+               return context.setRedirect(     atool, 'user/logged_in'
+                                                                 , came_from = came_from
+                                                                 , __ac_name=fg('member_id')
+                                                                 , __ac_password=fg('password'))
+               #return resp.redirect('%s?%s' % ( came_from, mq(__ac_name=fg('member_id'), __ac_password=fg('password'), noajax='1')) )
+       else:
+               options['member_id'] = fg('member_id')
+               options['password'] = fg('password')
+               options['form_action'] = target = atool.getActionInfo('user/logged_in')['url']
+               return context.confirm_join_template(**options)
+
+continuationFields = [
+         'given_name'
+       , 'name'
+       , 'member_email'
+       , 'member_id'
+       , 'password'
+       , 'confirm'
+       , 'send_password'
+       , 'billing_address'
+       , 'billing_city'
+       , 'billing_zipcode'
+       , 'country'
+       , 'phone'
+       , 'accept_gcs']
+
+
+for name in continuationFields :
+       options[name] = fg(name)
+
+# TODO try to be more clever...
+if not options['country']:
+       options['country'] = 'FR'
+options['came_from'] = fg('came_from')
+return context.customer_join_template(**options)
diff --git a/skins/customer_join_template.pt b/skins/customer_join_template.pt
new file mode 100644 (file)
index 0000000..7799f6f
--- /dev/null
@@ -0,0 +1,131 @@
+<html metal:use-macro="context/main_template/macros/master">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
+  </head>
+  <body metal:fill-slot="main_no_tabs" i18n:domain="realis"
+        tal:omit-tag="">
+    <div tal:condition="python:options.get('came_from', '').endswith('/my_cart')"
+         tal:define="step_authentication python:True" tal:omit-tag="">
+      <div metal:use-macro="here/sell_macros/macros/sell_steps"></div>
+    </div>
+    <h1 i18n:translate="">New customer account</h1>
+    
+    <form tal:attributes="action string:portal_url/customer_join_form" method="post">
+      <table class="TwoColumnForm">
+        <tr>
+          <td colspan="2" style="text-align:center">
+            <h3 i18n:translate="">Customer informations</h3>
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">First name</th>
+          <td>
+            <input type="text" name="given_name" size="30" value="" tal:attributes="value options/given_name" />
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">Last name</th>
+          <td>
+            <input type="text" name="name" size="30" value="" tal:attributes="value options/name" />
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">Email Address</th>
+          <td>
+            <input type="text" name="member_email" size="30"
+                   tal:attributes="value options/member_email" />
+          </td>
+        </tr>
+        <tr>
+         <th i18n:translate="">Member ID</th>
+         <td>
+          <input type="text" name="member_id" size="30" value=""
+                 tal:attributes="value options/member_id" />
+         </td>
+        </tr>
+        <tal:case tal:condition="not: options/validate_email">
+          <tr>
+            <th i18n:translate="">Password</th>
+            <td>
+              <input type="password" name="password" size="30" tal:attributes="value options/password" />
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Password (confirm)</th>
+            <td>
+              <input type="password" name="confirm" size="30" tal:attributes="value options/confirm" />
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Mail Password?</th>
+            <td>
+              <input type="checkbox" name="send_password" id="cb_send_password"
+                     tal:attributes="checked options/send_password" />
+              <em><label for="cb_send_password" i18n:translate="">Check this box to
+                have the password mailed.</label></em>
+            </td>
+          </tr>
+        </tal:case>
+        <tr>
+          <td colspan="2"><hr/></td>
+        </tr>
+        <tr>
+          <td colspan="2" style="text-align:center">
+            <h3 i18n:translate="">Billing informations</h3>
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">Address</th>
+          <td>
+            <textarea name="billing_address" tal:content="options/billing_address"
+                      cols="30" rows="1" style="width:auto"></textarea>
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">City</th>
+          <td>
+            <input type="text" name="billing_city" size="35" tal:attributes="value options/billing_city"/>
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">Zip code</th>
+          <td>
+            <input type="text" name="billing_zipcode" size="5" tal:attributes="value options/billing_zipcode"/>
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">Country</th>
+          <td>
+            <select name="country"
+                    tal:define="countries python:modules['Products.iso_3166_1'].fr.countries"
+                    i18n:domain="iso_3166_1">
+              <option tal:repeat="c countries" tal:attributes="value python:c[0]; selected python:c[0]==options['country']" tal:content="python:c[0]" i18n:translate=""></option>
+            </select>
+          </td>
+        </tr>
+        <tr>
+          <th i18n:translate="">Phone</th>
+          <td>
+            <input type="text" name="phone" tal:attributes="value options/phone"/>
+          </td>
+        </tr>
+        <tr>
+          <th><br/></th>
+          <td>
+            <input type="checkbox" name="accept_gcs" tal:attributes="checked python:options['accept_gcs']" />
+            <a tal:attributes="href string:$portal_url/cgv" target="_blank" i18n:translate="">I accept general conditions of sales</a>
+          </td>
+        </tr>
+        <tr>
+          <td>&nbsp;</td>
+          <td>
+            <input type="hidden" name="noAjax" value="1"/>
+            <input type="hidden" name="came_from" tal:condition="options/came_from" tal:attributes="value options/came_from" />
+            <input type="submit" name="add" value="Register" i18n:attributes="value"/>
+            <input type="submit" name="cancle" value="Cancel" i18n:attributes="value" style="margin-left:15em"/>
+          </td>
+        </tr>
+      </table>
+    </form>
+  </body>
+</html>
diff --git a/skins/customer_login_form.pt b/skins/customer_login_form.pt
new file mode 100644 (file)
index 0000000..788fcb4
--- /dev/null
@@ -0,0 +1,100 @@
+<html xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      metal:use-macro="here/main_template/macros/master">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
+  </head>
+  <body metal:fill-slot="main_no_tabs" i18n:domain="realis" tal:omit-tag="">
+    <div tal:condition="python:request.get('came_from', '').endswith('/my_cart')"
+         tal:define="step_authentication python:True"
+         tal:omit-tag="">
+      <div metal:use-macro="here/sell_macros/macros/sell_steps"></div>
+    </div>
+    <table width="70%">
+      <tr>
+        <td width="50%">
+          <h2 i18n:translate="">You already have an account?</h2>
+          <div i18n:translate="">Please login to continue</div>
+        </td>
+        <td width="50%">
+          <h2 i18n:translate="">New customer?</h2>
+          <div i18n:translate="">Welcome!<br/>
+          Please create an account to order.</div>
+      </tr>
+      <tr>
+        <td>
+          <form method="post" tal:attributes="action string:${here/portal_url}/logged_in">
+            <input type="hidden" name="noAjax" value="1" />
+
+            <!-- ****** Enable the automatic redirect ***** -->
+            <span tal:condition="exists: request/came_from">
+              <input type="hidden" name="came_from" value=""
+                     tal:attributes="value request/came_from" />
+            </span>
+            <!-- ****** Enable the automatic redirect ***** -->
+            <input type="hidden" name="just_login" value="True" />
+            <table class="TwoColumnForm">
+              <tr>
+                <th i18n:translate="user_name">Login</th>
+                <td>
+                  <input type="text" name="__ac_name" size="20" value=""
+                         tal:attributes="value python: request.get('__ac_name') or ''" />
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Password</th>
+                <td>
+                  <input type="password" name="__ac_password" size="20" />
+                </td>
+              </tr>
+              <tr>
+                <td><br/></td>
+                <td>
+                  <label>
+                    <input type="checkbox" name="__ac_persistent" checked="checked" />
+                    <span i18n:translate="" tal:omit-tag="">Remember my name.</span>
+                  </label>
+                </td>
+              </tr>
+
+              <tr>
+                <td><br/></td>
+                <td>
+                  <input type="submit" name="submit" value=" Login "
+                         i18n:attributes="value" />
+                </td>
+              </tr>
+
+            </table>
+          </form>
+        </td>
+        <td>
+          <form method="get" tal:attributes="action string:${here/portal_url}/customer_join_form">
+            <input type="submit" value="Open new account &gt;&gt;" i18n:attributes="value"/>
+            <input type="hidden" name="came_from" tal:condition="exists: request/came_from"
+                   tal:attributes="value request/came_from" />
+          </form>
+        </td>
+      </tr>
+    </table>
+          
+    <p><a href=""
+        tal:attributes="href string:${here/portal_url}/mail_password_form"
+        i18n:translate=""
+       >Forgot your password?</a>
+    </p>
+
+    <p i18n:translate="">Having trouble logging in? Make sure to enable cookies in
+        your web browser.
+    </p>
+    <p i18n:translate="">Don't forget to logout or exit your browser when you're
+      done.
+    </p>
+
+    <p i18n:translate="">Setting the 'Remember my name' option will set a cookie
+      with your username, so that when you next log in, your user name will
+      already be filled in for you.
+    </p>
+  </body>
+</html>
+
diff --git a/skins/get_photo_print_order_template.pt b/skins/get_photo_print_order_template.pt
new file mode 100644 (file)
index 0000000..c8ff9bc
--- /dev/null
@@ -0,0 +1,6 @@
+<tal:block
+  tal:define="o options/o;
+              classRowName options/classRowName;
+              portal_url options/portal_url">
+  <metal:block metal:use-macro="here/photoprint_templates_edit_template/macros/print_order_template_row"/>
+</tal:block>
\ No newline at end of file
diff --git a/skins/my_orders.py b/skins/my_orders.py
new file mode 100755 (executable)
index 0000000..5d3e6da
--- /dev/null
@@ -0,0 +1,75 @@
+##parameters=b_start=0, key='created', reverse=False
+from Products.CMFCore.utils import getToolByName
+from Products.Plinn.PloneMisc import Batch
+from Products.photoprint.utils import Message as _
+from ZTUtils import make_query
+
+wtool = getToolByName(context, 'portal_workflow')
+ctool = getToolByName(context, 'portal_catalog')
+mtool = getToolByName(context, 'portal_membership')
+utool = getToolByName(context, 'portal_url')
+portal = utool.getPortalObject()
+portal_url = utool()
+member = mtool.getAuthenticatedMember()
+options = {}
+
+columns = ( {'key': 'created',
+                        'title': _('Date'),
+                        'width': None,
+                        'colspan': None}
+                       , {'key': 'id',
+                        'title': _('Reference'),
+                        'width': None,
+                        'colspan': None}
+                       , {'key': 'quantity',
+                        'title': _('Prints'),
+                        'width': None,
+                        'colspan': None}
+                       , {'key': 'amount',
+                        'title': _('Amount'),
+                        'width': None,
+                        'colspan': None }
+                       , {'key': 'state',
+                        'title': _('State'),
+                        'width': None,
+                        'colspan': None }
+                       )
+
+target = context.absolute_url()
+
+for column in columns :
+       column['url'] = None
+       column['images'] = None
+
+options['columns'] = columns
+
+
+orders = ctool(portal_type='Order', listCreators=member.getId(), sort_on='created', sort_order='reverse')
+
+def beforeGetItem(item) :
+       item = item.getObject()
+       info = {}
+       info['url'] = item.absolute_url()
+       info['created'] = item.created()
+       info['reference'] = item.getId()
+       info['quantity'] = item.quantity
+       info['price'] = item.amountWithFees
+       info['state'] = wtool.getInfoFor(item, 'review_state', wf_id='order_workflow')
+       return info
+       
+orders = Batch(orders, 20, b_start, orphan=0, quantumleap=1, before_getitem=beforeGetItem)
+options['orders'] = orders
+
+breadcrumbs = [
+       { 'id'          : 'root'
+       , 'title'       : portal.title
+       , 'url'    : portal_url},
+       
+       {'id'           : 'my_orders'
+        ,'title'       : _('My orders')
+        , 'url'        : '%s/my_orders' % portal_url}
+       ]
+
+options['breadcrumbs'] = breadcrumbs
+
+return context.my_orders_template(**options)
diff --git a/skins/my_orders_template.pt b/skins/my_orders_template.pt
new file mode 100644 (file)
index 0000000..b8c72f9
--- /dev/null
@@ -0,0 +1,40 @@
+<html metal:use-macro="here/main_template/macros/master"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  <head>
+    <title>Order listing</title>
+    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
+    
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs" tal:omit-tag=""
+         tal:define="orders options/orders;
+                     columns options/columns">
+      <div tal:condition="orders" tal:omit-tag="">
+        <div tal:define="batch orders" tal:omit-tag="">
+          <span metal:use-macro="here/batch_macros/macros/navigation">batch navigation</span>
+        </div>
+        <table class="listing" cellspacing="0">
+          <thead id="FolderListingHeader">
+                               <tr>
+                                       <th tal:attributes="width column_info/width; colspan column_info/colspan"
+                                           tal:repeat="column_info columns" nowrap="nowrap">
+                                         <span tal:replace="column_info/title">title</span>
+                                       </th>
+                               </tr>
+                       </thead>
+          <tr tal:repeat="order orders" tal:attributes="class python:repeat['order'].odd() and 'odd' or 'even'">
+            <td tal:content="python:order['created'].strftime(locale_date_fmt)"></td>
+            <td>
+              <a tal:content="order/reference" tal:attributes="href order/url"></a>
+            </td>
+            <td tal:content="order/quantity"></td>
+            <td tal:content="order/price/taxed"></td>
+            <td tal:content="order/state" i18n:translate=""></td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </body>
+</html>
diff --git a/skins/order_cancel_form.pt b/skins/order_cancel_form.pt
new file mode 100644 (file)
index 0000000..b976045
--- /dev/null
@@ -0,0 +1,66 @@
+<html metal:use-macro="here/main_template/macros/master">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs"
+         tal:define="getToolByName python:modules['Products.CMFCore.utils'].getToolByName;
+                     wtool python:getToolByName(here, 'portal_workflow');
+                     review_state python: wtool.getInfoFor(here, 'review_state', wf_id='order_workflow')"
+         tal:omit-tag="">
+      <h2 i18n:translate="">Cancel order "<span tal:replace="here/title_or_id" i18n:name="order_reference">Item</span>"</h2>
+       <div i18n:translate="">
+         Cancel the order and relist reserved copies.
+       </div>
+    
+      <form method="post" tal:attributes="action string:${here/absolute_url}/content_status_modify">
+        <p>
+          <span i18n:translate="" tal:omit-tag="">Current state:</span>
+          <span tal:content="review_state" i18n:translate="" tal:omit-tag="">state</span>
+        </p>
+        <table class="TwoColumnForm">
+          <tr>
+            <th i18n:translate="mail_subject">Subject</th>
+            <td tal:define="_ python:modules['Products.photoprint.utils'].translate">
+              <input type="text" size="58"
+                name="subject"
+                tal:attributes="value python:_('[%s] order %s canceling notification', here) % (here.portal_photo_print.store_name, here.getId())"/>
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Comments</th>
+            <td>
+              <textarea name="comment" cols="50" rows="8" i18n:translate=""
+               >Due to a lack of payment since <span
+                  tal:content="python:here.created().strftime(locale_date_fmt)"
+                  tal:omit-tag=""
+                  i18n:name="creation_date"/>, your order has been canceled.
+                
+                The <span tal:content="here/portal_photo_print/store_name|nothing"
+                          tal:omit-tag=""
+                          i18n:name="store_name"></span> team.
+              </textarea>
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Send email</th>
+            <td>
+              <label tal:repeat="member python:mtool.getMembers(here.listCreators())">
+                <input type="checkbox" name="recipients:list" tal:attributes="value member/id" checked="checked" />
+                <span tal:replace="member/getMemberFullName">Membre full name</span>
+                &lt;<span tal:replace="member/email">member email</span>&gt;
+              </label>
+            </td>
+          </tr>
+          <tr>
+            <td><br/></td>
+            <td>
+              <input type="hidden" name="syncFragments:tokens" value="rightCell" />
+              <input type="hidden" name="workflow_action" value="cancel" />
+              <input type="submit" value="Cancel the order" i18n:attributes="value" />
+            </td>
+        </table>
+      </form>
+    </div>
+  </body>
+</html>
diff --git a/skins/order_list.py b/skins/order_list.py
new file mode 100755 (executable)
index 0000000..37a0e4c
--- /dev/null
@@ -0,0 +1,95 @@
+##parameters=b_start=0, key='created', reverse=False
+from Products.CMFCore.utils import getToolByName
+from Products.Plinn.PloneMisc import Batch
+from Products.photoprint.utils import Message as _
+from ZTUtils import make_query
+
+wtool = getToolByName(context, 'portal_workflow')
+mtool = getToolByName(context, 'portal_membership')
+options = {}
+folders = context.contentValues({'portal_type':'Order Folder'})
+options['folders'] = folders
+
+columns = ( {'key': 'created',
+                        'title': _('Date'),
+                        'width': None,
+                        'colspan': None}
+                       , {'key': 'customer',
+                        'title': _('Customer'),
+                        'width': None,
+                        'colspan': None}
+                       , {'key': 'id',
+                        'title': _('Reference'),
+                        'width': None,
+                        'colspan': None}
+                       , {'key': 'quantity',
+                        'title': _('Prints'),
+                        'width': None,
+                        'colspan': None}
+                       , {'key': 'amount',
+                        'title': _('Amount'),
+                        'width': None,
+                        'colspan': None }
+                       , {'key': 'state',
+                        'title': _('State'),
+                        'width': None,
+                        'colspan': None }
+                       )
+
+target = context.absolute_url()
+
+for column in columns :
+       images = []
+       if key == column['key']:
+               if reverse :
+                       toggleImg = getattr(context, 'arrowDown.gif')
+                       alt = _('descending sort')
+               else :
+                       toggleImg = getattr(context, 'arrowUp.gif')
+                       alt = _('ascending sort')
+               query = make_query(key=column['key'], reverse = not reverse)
+               images.append( {'src' : toggleImg.absolute_url(), 'alt' : alt} )
+       else :
+               query = make_query(key=column['key'], reverse = reverse)
+       
+       column['url'] = '%s?%s' % (target, query)
+       column['images'] = images
+
+options['columns'] = columns
+
+def getReviewState(item) :
+       return wtool.getInfoFor(item, 'review_state', wf_id='order_workflow')
+
+sortFuncs = {'created' : lambda a, b : cmp(a.created(), b.created())
+                       ,'customer'     : lambda a, b : cmp(a.Creator(), b.Creator())
+                       ,'id'           : lambda a, b : cmp(a.getId(), b.getId())
+                       ,'quantity'     : lambda a, b : cmp(a.quantity, b.quantity)
+                       ,'amount'       : lambda a, b : cmp(a.amountWithFees.getValues()['taxed'], b.amountWithFees.getValues()['taxed'])
+                       ,'state'        : lambda a, b : cmp(getReviewState(a), getReviewState(b))}
+
+orders = context.contentValues({'portal_type':'Order'})
+step = reverse and -1 or 1
+orders.sort(sortFuncs[key])
+orders = orders[::step]
+options['key'] = key
+options['reverse'] = reverse
+
+def beforeGetItem(item) :
+       info = {}
+       info['url'] = item.absolute_url()
+       info['created'] = item.created()
+       info['reference'] = item.getId()
+       info['quantity'] = item.quantity
+       info['price'] = item.amountWithFees
+       info['state'] = wtool.getInfoFor(item, 'review_state', wf_id='order_workflow')
+       customer = mtool.getMemberById(item.Creator())
+       if customer :
+               info['customer'] = customer.getMemberFullName()
+       else :
+               info['customer'] = item.Creator()
+       return info
+       
+orders = Batch(orders, 40, b_start, orphan=0, quantumleap=1, before_getitem=beforeGetItem)
+options['orders'] = orders
+
+return context.order_list_template(**options)
diff --git a/skins/order_list_template.pt b/skins/order_list_template.pt
new file mode 100644 (file)
index 0000000..ac82b0d
--- /dev/null
@@ -0,0 +1,56 @@
+<html metal:use-macro="here/main_template/macros/master"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  <head>
+    <title>Order listing</title>
+    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
+    
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main" tal:omit-tag=""
+         tal:define="folders options/folders;
+                     orders options/orders;
+                     columns options/columns">
+      <table class="listing" cellspacing="0" tal:condition="folders">
+        <tr>
+          <th i18n:translate="">Name</th>
+        </tr>
+        <tr tal:repeat="folder folders" tal:attributes="class python:repeat['folder'].odd() and 'odd' or 'even'">
+          <td>
+            <a tal:content="folder/title_or_id" tal:attributes="href folder/absolute_url"></a>
+          </td>
+        </tr>
+      </table>
+      <div tal:condition="orders" tal:omit-tag="">
+        <div tal:define="batch orders" tal:omit-tag="">
+          <span metal:use-macro="here/batch_macros/macros/navigation">batch navigation</span>
+        </div>
+        <table class="listing" cellspacing="0">
+          <thead id="FolderListingHeader">
+                               <tr>
+                                       <th tal:attributes="width column_info/width; colspan column_info/colspan" tal:repeat="column_info columns" nowrap="nowrap" >
+                                               <a href="." tal:attributes="href column_info/url" tal:content="column_info/title" i18n:translate="">Type</a>
+                                               <span  tal:repeat="img column_info/images" tal:omit-tag="">
+                                                       <a tal:omit-tag="python:not img.has_key('href')"
+                                                                tal:attributes="href img/href|nothing ; title img/alt" i18n:attributes="title"
+                                                                ><img tal:attributes="src img/src ; alt img/alt ; id img/id|nothing" border="0" i18n:attributes="alt" /></a>
+                                               </span>
+                                       </th>
+                               </tr>
+                       </thead>
+          <tr tal:repeat="order orders" tal:attributes="class python:repeat['order'].odd() and 'odd' or 'even'">
+            <td tal:content="python:order['created'].strftime(locale_date_fmt)">date</td>
+            <td tal:content="order/customer">customer</td>
+            <td>
+              <a tal:content="order/reference" tal:attributes="href order/url">reference</a>
+            </td>
+            <td tal:content="order/quantity">quantity</td>
+            <td tal:content="order/price/taxed">price</td>
+            <td tal:content="order/state" i18n:translate="">state</td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </body>
+</html>
diff --git a/skins/order_manual_payment_form.pt b/skins/order_manual_payment_form.pt
new file mode 100644 (file)
index 0000000..827be27
--- /dev/null
@@ -0,0 +1,33 @@
+<html metal:use-macro="here/main_template/macros/master">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs"
+         tal:define="getToolByName python:modules['Products.CMFCore.utils'].getToolByName;
+                     wtool python:getToolByName(here, 'portal_workflow');
+                     review_state python: wtool.getInfoFor(here, 'review_state', wf_id='order_workflow')"
+         tal:omit-tag="">
+      <h2 i18n:translate="">Pay order "<span tal:replace="here/title_or_id" i18n:name="order_reference">Item</span>" manually.</h2>
+       <div i18n:translate="">
+         Pay manually
+       </div>
+    
+      <form method="post" tal:attributes="action string:${here/absolute_url}/content_status_modify">
+        <p>
+          <span i18n:translate="" tal:omit-tag="">Current state:</span>
+          <span tal:content="review_state" i18n:translate="" tal:omit-tag="">state</span>
+        </p>
+        <h3 i18n:translate="">Payment description</h3>
+        <textarea name="comment" cols="60" rows="5"></textarea>
+        <dl class="FieldHelp">
+          <dd i18n:translate="">Please indicate payment mean and references</dd>
+        </dl>
+        <br/>
+        <input type="hidden" name="syncFragments:tokens" value="rightCell" />
+        <input type="hidden" name="workflow_action" value="manual_payment" />
+        <input type="submit" value="Pay" i18n:attributes="value" />
+      </form>
+    </div>
+  </body>
+</html>
diff --git a/skins/order_notify_done_form.pt b/skins/order_notify_done_form.pt
new file mode 100644 (file)
index 0000000..d764c64
--- /dev/null
@@ -0,0 +1,30 @@
+<html metal:use-macro="here/main_template/macros/master">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs"
+         tal:define="getToolByName python:modules['Products.CMFCore.utils'].getToolByName;
+                     wtool python:getToolByName(here, 'portal_workflow');
+                     review_state python: wtool.getInfoFor(here, 'review_state', wf_id='order_workflow')"
+         tal:omit-tag="">
+      <h2 i18n:translate="">Notify order "<span tal:replace="here/title_or_id" i18n:name="order_reference">Item</span>" done.</h2>
+       <div i18n:translate="">
+         Notify that order has been made.
+       </div>
+    
+      <form method="post" tal:attributes="action string:${here/absolute_url}/content_status_modify">
+        <p>
+          <span i18n:translate="" tal:omit-tag="">Current state:</span>
+          <span tal:content="review_state" i18n:translate="" tal:omit-tag="">state</span>
+        </p>
+        <h3 i18n:translate="">Comments</h3>
+        <textarea name="comment" cols="60" rows="5"></textarea>
+        <br/>
+        <input type="hidden" name="syncFragments:tokens" value="rightCell" />
+        <input type="hidden" name="workflow_action" value="notify_done" />
+        <input type="submit" value="Notify" i18n:attributes="value" />
+      </form>
+    </div>
+  </body>
+</html>
diff --git a/skins/order_notify_sent_form.pt b/skins/order_notify_sent_form.pt
new file mode 100644 (file)
index 0000000..aaf1a16
--- /dev/null
@@ -0,0 +1,79 @@
+<html metal:use-macro="here/main_template/macros/master">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs"
+         tal:define="getToolByName python:modules['Products.CMFCore.utils'].getToolByName;
+                     wtool python:getToolByName(here, 'portal_workflow');
+                     review_state python: wtool.getInfoFor(here, 'review_state', wf_id='order_workflow')"
+         tal:omit-tag="">
+      <h2 i18n:translate="">Notify order "<span tal:replace="here/title_or_id" i18n:name="order_reference">Item</span>" sent.</h2>
+       <div i18n:translate="">
+         Notify that order has been sent.
+       </div>
+    
+      <form method="post" tal:attributes="action string:${here/absolute_url}/content_status_modify">
+        <p>
+          <span i18n:translate="" tal:omit-tag="">Current state:</span>
+          <span tal:content="review_state" i18n:translate="" tal:omit-tag="">state</span>
+        </p>
+        <h3 i18n:translate="">Tracking info</h3>
+        <table class="TwoColumnForm">
+          <tr>
+            <th i18n:translate="mail_subject">Subject</th>
+            <td tal:define="_ python:modules['Products.photoprint.utils'].translate">
+              <input type="text" size="58"
+                name="subject"
+                tal:attributes="value python:_('[%s] order %s sending notification', here) % (here.portal_photo_print.store_name, here.getId())"/>
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Comments</th>
+            <td>
+              <textarea name="comment" cols="50" rows="8" i18n:translate="">
+                Your order has been sent. You will receive your prints soon.
+                You will be able to track your parcel with the informations
+                below.
+                
+                The <span tal:content="here/portal_photo_print/store_name|nothing"
+                          tal:omit-tag=""
+                          i18n:name="store_name"></span> team thanks you for your
+                confidence and wish you receipt you order.
+              </textarea>
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Tracking number</th>
+            <td>
+              <input type="text" name="tracking_number" value="" size="20" />
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Tracking url</th>
+            <td>
+              <input type="text" name="tracking_url" value="" size="58" />
+            </td>
+          </tr>
+          <tr>
+            <th i18n:translate="">Send email</th>
+            <td>
+              <label tal:repeat="member python:mtool.getMembers(here.listCreators())">
+                <input type="checkbox" name="recipients:list" tal:attributes="value member/id" checked="checked" />
+                <span tal:replace="member/getMemberFullName">Membre full name</span>
+                &lt;<span tal:replace="member/email">member email</span>&gt;
+              </label>
+            </td>
+          </tr>
+          <tr>
+            <td><br/></td>
+            <td>
+              <input type="hidden" name="syncFragments:tokens" value="rightCell" />
+              <input type="hidden" name="workflow_action" value="notify_sent" />
+              <input type="submit" value="Notify" i18n:attributes="value" />
+            </td>
+        </table>
+      </form>
+    </div>
+  </body>
+</html>
diff --git a/skins/order_payment_template.pt b/skins/order_payment_template.pt
new file mode 100644 (file)
index 0000000..1c9b847
--- /dev/null
@@ -0,0 +1,19 @@
+<html metal:use-macro="here/main_template/macros/master"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  <head>
+    <title>Payment template</title>
+    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
+    
+  </head>
+  <body>
+    <div metal:fill-slot="main_no_tabs" tal:omit-tag=""
+         tal:define="paymentRequest options/paymentRequest">
+      
+      <div tal:content="structure paymentRequest/FORM">
+        formulaire cyberplus
+      </div>
+    </div>
+  </body>
+</html>
diff --git a/skins/order_printing_list.py b/skins/order_printing_list.py
new file mode 100755 (executable)
index 0000000..ab7963a
--- /dev/null
@@ -0,0 +1,39 @@
+##parameters=
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.exceptions import zExceptions_Unauthorized
+uidh = getToolByName(context, 'portal_uidhandler')
+options = {}
+
+infos = []
+for item in context.items :
+       try :
+               b = uidh.getBrain(item['cmf_uid'])
+       except : # TODO: catch only UniqueIdError (not public yet:)
+               raise zExceptions_Unauthorized
+       size = b.getThumbnailSize
+       d = {'thumbUrl'         : '%s/getThumbnail' % b.getURL()
+               ,'thumbHeight'  : size['height'] / 2
+               ,'thumbWidth'   : size['width'] / 2
+               ,'alt'                  : ('%s - %s' % (b.Title, b.Description)).strip(' -')
+               ,'title'                : item['title']
+               ,'description'  : item['description']
+               ,'unit_price'   : item['unit_price']
+               ,'quantity'             : item['quantity']
+               ,'total'                : item['unit_price'] * item['quantity']
+               ,'url'                  : b.getURL()
+               ,'id'                   : b.getId
+               }
+       infos.append(d)
+
+options['infos'] = infos
+if traverse_subpath and traverse_subpath[-1] == 'xml' :
+       channel_info = {'title' :'Commande %s' % context.getId()
+                                       ,'url' : context.absolute_url() 
+                                       ,'description'  :'' # TODO mettre l'identification client
+                                       , }
+                                       
+       options['channel_info'] = channel_info
+       options['listItemInfos'] = infos
+       return context.order_printing_list_template_xml(**options)
+else :
+       return context.order_printing_list_template(**options)
\ No newline at end of file
diff --git a/skins/order_printing_list_template.pt b/skins/order_printing_list_template.pt
new file mode 100644 (file)
index 0000000..5f7688d
--- /dev/null
@@ -0,0 +1,45 @@
+<html metal:use-macro="here/main_template/macros/master"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  <head>
+    <title>order printing list</title>
+    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
+    <link metal:fill-slot="base"
+          rel="alternate" type="application/rss+xml"
+          tal:attributes="title here/title_or_id;
+                          href string:${here/absolute_url}/order_printing_list/xml" />
+    
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs" tal:omit-tag="">
+      RSS :
+      <input type="text"
+             tal:define="rss string:${context/absolute_url}/order_printing_list/xml"
+             tal:attributes="value rss; size python:len(rss)"/>
+      <table class="listing" cellspacing="0">
+        <tr>
+          <th i18n:translate="">Image</th>
+          <th i18n:translate="">File</th>
+          <th i18n:translate="">Format / type</th>
+          <th i18n:translate="">Quantity</th>
+        </tr>
+        <tr  tal:repeat="i options/infos" tal:attributes="class python:repeat['i'].odd() and 'odd' or 'even'">
+          <td>
+            <a tal:attributes="href i/url">
+              <img tal:attributes="src i/thumbUrl;
+                                   alt i/title;
+                                   height i/thumbHeight;
+                                   width  i/thumbWidth"/>
+            </a>
+          </td>
+          <td tal:content="i/id">image id</td>
+          <td tal:content="i/title" class="num">job title</td>
+          <td tal:content="i/quantity" class="num" style="border-right:1px solid black">quantity</td>
+        </tr>
+      </table>
+      
+      
+    </div>
+  </body>
+</html>
diff --git a/skins/order_printing_list_template_xml.pt b/skins/order_printing_list_template_xml.pt
new file mode 100644 (file)
index 0000000..2c41205
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<rdf:RDF
+    xmlns:tal="http://xml.zope.org/namespaces/tal"
+    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:pp="http://luxia.fr/namespaces/pp/"
+    xmlns="http://purl.org/rss/1.0/">
+  <channel rdf:about="URL"
+      tal:define="channel_info options/channel_info"
+      tal:attributes="rdf:about channel_info/url">
+    <title tal:content="channel_info/title">TITLE</title>
+    <link tal:content="channel_info/url">URL</link>
+    <description
+        tal:content="channel_info/description">DESCRIPTION</description>
+    <image rdf:resource="logo.gif" />
+    <items>
+      <rdf:Seq>
+        <rdf:li resource="URL"
+            tal:repeat="item_info options/listItemInfos"
+            tal:attributes="resource item_info/url" />
+      </rdf:Seq>
+    </items>
+  </channel>
+  <item rdf:about="URL"
+      tal:repeat="item_info options/listItemInfos"
+      tal:attributes="rdf:about item_info/url">
+    <title tal:content="item_info/id">TITLE</title>
+    <link tal:content="string:${item_info/url}/photo_download">URL</link>
+    <pp:title tal:content="item_info/title">TITLE</pp:title>
+    <pp:description
+        tal:condition="item_info/description"
+        tal:content="item_info/description">DESCRIPTION</pp:description>
+    <pp:quantity tal:content="item_info/quantity">QUANTITY</pp:quantity>
+  </item>
+</rdf:RDF>
diff --git a/skins/order_states/canceled-en.gif b/skins/order_states/canceled-en.gif
new file mode 100644 (file)
index 0000000..3d08da5
Binary files /dev/null and b/skins/order_states/canceled-en.gif differ
diff --git a/skins/order_states/canceled-fr.gif b/skins/order_states/canceled-fr.gif
new file mode 100644 (file)
index 0000000..a72a57f
Binary files /dev/null and b/skins/order_states/canceled-fr.gif differ
diff --git a/skins/order_states/done-en.gif b/skins/order_states/done-en.gif
new file mode 100644 (file)
index 0000000..7473727
Binary files /dev/null and b/skins/order_states/done-en.gif differ
diff --git a/skins/order_states/done-fr.gif b/skins/order_states/done-fr.gif
new file mode 100644 (file)
index 0000000..46cf576
Binary files /dev/null and b/skins/order_states/done-fr.gif differ
diff --git a/skins/order_states/paid-en.gif b/skins/order_states/paid-en.gif
new file mode 100644 (file)
index 0000000..0bc95ed
Binary files /dev/null and b/skins/order_states/paid-en.gif differ
diff --git a/skins/order_states/paid-fr.gif b/skins/order_states/paid-fr.gif
new file mode 100644 (file)
index 0000000..bae27ca
Binary files /dev/null and b/skins/order_states/paid-fr.gif differ
diff --git a/skins/order_states/recorded-en.gif b/skins/order_states/recorded-en.gif
new file mode 100644 (file)
index 0000000..685be0e
Binary files /dev/null and b/skins/order_states/recorded-en.gif differ
diff --git a/skins/order_states/recorded-fr.gif b/skins/order_states/recorded-fr.gif
new file mode 100644 (file)
index 0000000..f305f59
Binary files /dev/null and b/skins/order_states/recorded-fr.gif differ
diff --git a/skins/order_states/refused-en.gif b/skins/order_states/refused-en.gif
new file mode 100644 (file)
index 0000000..b52e0b5
Binary files /dev/null and b/skins/order_states/refused-en.gif differ
diff --git a/skins/order_states/refused-fr.gif b/skins/order_states/refused-fr.gif
new file mode 100644 (file)
index 0000000..2954c7b
Binary files /dev/null and b/skins/order_states/refused-fr.gif differ
diff --git a/skins/order_states/sent-en.gif b/skins/order_states/sent-en.gif
new file mode 100644 (file)
index 0000000..cd070a8
Binary files /dev/null and b/skins/order_states/sent-en.gif differ
diff --git a/skins/order_states/sent-fr.gif b/skins/order_states/sent-fr.gif
new file mode 100644 (file)
index 0000000..0c1c129
Binary files /dev/null and b/skins/order_states/sent-fr.gif differ
diff --git a/skins/order_view.py b/skins/order_view.py
new file mode 100755 (executable)
index 0000000..5ed50bf
--- /dev/null
@@ -0,0 +1,62 @@
+##parameters=
+from Products.CMFCore.utils import getToolByName
+from Products.photoprint.price import Price
+from Products.photoprint.cart import PrintCart
+uidh = getToolByName(context, 'portal_uidhandler')
+wtool = getToolByName(context, 'portal_workflow')
+
+options = {}
+
+quantity = 0
+prices = []
+infos = []
+
+session = context.REQUEST.SESSION
+sg = session.get
+cart = sg('cart', PrintCart())
+wfstate = wtool.getInfoFor(context, 'review_state', 'order_workflow')
+options['wfstate'] = wfstate
+options['wfhistory'] = wtool.getInfoFor(context, 'review_history', 'order_workflow')
+toBePaid = wfstate == 'recorded'
+
+if toBePaid :
+       paymentRequest = context.getPaymentRequest()
+       options['paymentRequest'] = paymentRequest
+
+if cart.locked and \
+       cart.pendingOrderPath == context.getPhysicalPath() :
+       options['orderIsCart'] = True
+       if wfstate != 'recorded' :
+               cart = PrintCart()
+               session['cart'] = cart
+else :
+       options['orderIsCart'] = False
+
+for item in context.items :
+       d = {'title'            : item['title']
+               ,'description'  : item['description']
+               ,'unit_price'   : item['unit_price']
+               ,'quantity'             : item['quantity']
+               ,'total'                : item['unit_price'] * item['quantity']
+               }
+
+       b = uidh.queryBrain(item['cmf_uid'])
+       if b :
+               size = b.getThumbnailSize
+               thumbInfo = {'thumbUrl'         : '%s/getThumbnail' % b.getURL()
+                                       ,'thumbHeight'  : size['height'] / 2
+                                       ,'thumbWidth'   : size['width'] / 2
+                                       ,'alt'                  : ('%s - %s' % (b.Title, b.Description)).strip(' -')
+                                       }
+               d.update(thumbInfo)
+       quantity += d['quantity']
+       prices.append(d['total'])
+       infos.append(d)
+
+options['infos'] = infos
+options['quantity'] = quantity
+options['pricesSum'] = context.price
+options['shippingFees'] = shippingFees = context.shippingFees
+options['total'] = context.amountWithFees
+
+return context.order_view_template(**options)
\ No newline at end of file
diff --git a/skins/order_view_template.pt b/skins/order_view_template.pt
new file mode 100644 (file)
index 0000000..f5876b7
--- /dev/null
@@ -0,0 +1,207 @@
+<html metal:use-macro="here/main_template/macros/master"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  <head>
+    <title>titre</title>
+    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
+    
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main_no_tabs" tal:omit-tag="">
+      <div tal:condition="options/orderIsCart" tal:omit-tag="">
+        <div tal:define="step_authentication python:True;
+                         step_shipping python:True;
+                         step_payment python:True;
+                         step_confirmation python:options['wfstate'] != 'recorded'">
+          <div metal:use-macro="here/sell_macros/macros/sell_steps">
+            sell steps macro
+          </div>
+        </div>
+      </div>
+      <h1 i18n:translate="" style="margin-bottom:0">
+        Order Nb. <span tal:content="here/getId" tal:omit-tag="" i18n:name="order_number"/>
+      </h1>
+      <table cellpadding="5">
+        <tr>
+          <td colspan="50%">
+            <h2 i18n:translate="" style="margin:0">Billing</h2>
+            <table tal:define="billing here/billing" class="listing">
+              <tr class="odd">
+                <th i18n:translate="">Name</th>
+                <td tal:content="billing/name">billing name</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Address</th>
+                <td tal:content="billing/address">billin address</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">City</th>
+                <td tal:content="billing/city">billing city</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Zip code</th>
+                <td tal:content="billing/zipcode">billing zip code</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Country</th>
+                <td tal:content="billing/country" i18n:domain="iso_3166_1" i18n:translate="">Country</td>
+              </tr>
+            </table>
+          </td>
+          <td>
+            <h2 i18n:translate="" style="margin:0">Shipping</h2>
+            <table tal:define="shipping here/shipping" class="listing">
+              <tr>
+                <th i18n:translate="">Name</th>
+                <td tal:content="shipping/name">shpping name</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Address</th>
+                <td tal:content="shipping/address">shpping address</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">City</th>
+                <td tal:content="shipping/city">shipping city</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Zip code</th>
+                <td tal:content="shipping/zipcode">shipping zip code</td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Country</th>
+                <td tal:content="shipping/country" i18n:domain="iso_3166_1" i18n:translate="">chipping country</td>
+              </tr>
+            </table>
+          </td>
+          <td>
+            <img tal:define="translate nocall:modules/Products/photoprint/utils/translate;
+                             tr python:lambda msg: translate(msg, here)"
+                 tal:attributes="src python:'%s/order_states/%s' % (portal_url, tr('%s-en.gif' % options['wfstate']))"
+                 style="float:right"/>
+          </td>
+          <td tal:condition="workflow_actions" class="boxes_container">
+            <h3 i18n:translate="">Processing</h3>
+            <ul>
+              <li tal:repeat="action workflow_actions">
+                <a tal:attributes="href action/url" tal:content="action/title" i18n:translate=""></a>
+              </li>
+            </ul>
+          </td>
+        </tr>
+      </table>
+      <table class="listing" cellspacing="0">
+        <col span="2"/>
+        <col span="4" style="text-align:right"/>
+        <tr>
+          <th i18n:translate="">Image</th>
+          <th i18n:translate="" style="width:30em">Printing format and type</th>
+          <th i18n:translate="">Quantity</th>
+          <th i18n:translate="">Unit price</th>
+          <th i18n:translate="">VAT (%)</th>
+          <th i18n:translate="">Amount<br/>(tax incl.)</th>
+        </tr>
+        <tr tal:repeat="i options/infos" tal:attributes="class python:repeat['i'].odd() and 'odd' or 'even'">
+          <td>
+            <img border="0" tal:condition="python:i.has_key('thumbUrl')"
+              tal:attributes="src i/thumbUrl;
+                              alt i/title;
+                              height i/thumbHeight;
+                              width  i/thumbWidth"/>
+            <em tal:condition="python:not i.has_key('thumbUrl')" i18n:translate="">image removed</em>
+          </td>
+          <td>
+            <div style="font-weight:bold" tal:content="i/title"></div>
+            <dl class="FieldHelp">
+             <dd tal:content="i/description"></dd>
+            </dl>
+          </td>
+          <td class="num" tal:content="python:i['quantity']">12</td>
+          <td class="num" tal:content="python:'%s €' % i['unit_price'].value"></td>
+          <td class="num" tal:content="python:i['unit_price'].vat"></td>
+          <td class="num" tal:content="python:'%s €' % i['total'].taxed" style="border-right:1px solid black"></td>
+        </tr>
+        <tbody class="total">
+          <tr>
+            <th colspan="2"></th>
+            <th class="num" tal:content="options/quantity"></th>
+            <th class="num">—</th>
+            <th class="num">—</th>
+            <th class="num" tal:content="python:'%s €' % options['pricesSum'].taxed"
+                style="border-right:1px solid black"></th>
+          </tr>
+          <tr>
+            <td class="num" colspan="5" i18n:translate="">Shipping</td>
+            <td class="num" tal:content="python:'%s €' % options['shippingFees'].taxed"></td>
+          </tr>
+          <tr>
+            <td class="num" colspan="5" i18n:translate="">VAT</td>
+            <td class="num" tal:content="python:'%s €' % options['total'].tax"></td>
+          </tr>
+          <tr>
+            <th class="num" colspan="5" i18n:translate="">Total amount to pay</th>
+            <td class="num" style="color:#f28c18"
+                tal:content="python:'%s €' % options['total'].taxed">montant total</td>
+          </tr>
+        </tbody>
+        <tfoot>
+          <tr tal:define="paymentRequest options/paymentRequest|nothing" tal:condition="paymentRequest">
+            <td><br/></td>
+            <td colspan="5">
+              <div style="text-align:right">
+                <form tal:attributes="action paymentRequest/URL" method="post" style="text-align:top">
+                  <input type="hidden" name="DATA" tal:attributes="value paymentRequest/DATA"/>
+                  <span i18n:translate="" style="font-size:110%;font-weight:bold;vertical-align:middle">Use one of these button to pay:</span>
+                  <span tal:repeat="mean paymentRequest/PAYMENT_MEANS">
+                    <input type="image" style="vertical-align:middle"
+                      tal:attributes="src python:'%s/cyberplus_logo/%s.gif' % (portal_url, mean);
+                                      name mean"/>    
+                  </span>
+                  <input type="hidden" name="noAjax" value="1"/>
+                </form>
+              </div>
+              <div>
+                <img tal:attributes="src string:$portal_url/cyberplus_logo/banque_populaire_logo.png"
+                     style="float:left;margin-right:1em"/>
+                <dl class="FieldHelp">
+                  <dd i18n:translate="">
+                    Please click over the button representing your credit card.
+                    You will leave temporarily this web site to pay your order
+                    on our bank partner payment site. After your payment, you
+                    will be able to come back to the store and get an invoice of
+                    your transaction.
+                  </dd>
+                  <dd i18n:translate="">
+                    This secured payment is provided by the Cyberplus™ payment
+                    service of "Banque Populaire".
+                  </dd>
+                </dl>
+              </div>
+            </td>
+          </tr>
+        </tfoot>
+      </table>
+      <br/>
+      <h3 i18n:translate="">Order processing history</h3>
+      <table cellspacing="0" class="listing"
+             tal:define="history options/wfhistory"
+             tal:condition="history">
+        <tr>
+          <th i18n:translate="">Date</th>
+          <th i18n:translate="">Actor</th>
+          <th i18n:translate="">Action</th>
+        </tr>
+        <div tal:repeat="item python:history[::-1]" tal:omit-tag="">
+               <tr tal:define="oddrow repeat/item/odd" tal:attributes="class python:test(oddrow, 'even', 'odd')"
+                               tal:condition="item/action">
+                       <td tal:content="python:item['time'].strftime(locale_date_fmt)">time</td>
+                       <td tal:condition="python:item['action'].startswith('auto_')">Cyberplus</td>
+                       <td tal:condition="python:not item['action'].startswith('auto_')"
+                           tal:content="python:mtool.getMemberFullNameById(item['actor'], nameBefore=0)">actor</td>
+            <td tal:content="item/action" i18n:translate=""></td>
+          </tr>
+        </div>
+      </table>
+    </div>
+  </body>
+</html>
diff --git a/skins/personalize_form.pt b/skins/personalize_form.pt
new file mode 100755 (executable)
index 0000000..a82bba1
--- /dev/null
@@ -0,0 +1,101 @@
+<html xmlns="http://www.w3.org/1999/xhtml" metal:use-macro="here/main_template/macros/master">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
+  </head>
+
+  <body>
+    <div metal:fill-slot="main_no_tabs" i18n:domain="photoprint">
+      <div tal:define="purl here/portal_url;
+                  mtool here/portal_membership;
+                  member mtool/getAuthenticatedMember;
+                 ">
+        <div tal:condition="python: not( mtool.checkPermission( 'Set own properties'
+                                                        , here ) )">
+          <span id="dummy_for_redirect" tal:define="aurl here/absolute_url;
+                      rurl string:${purl}/login_form?came_from=${aurl};
+                      response request/RESPONSE;
+                      redirect python:response.redirect( rurl )" />
+        </div>
+        <!-- not Set own properties -->
+        <div class="config">
+          <h1 i18n:translate="">Member Preferences</h1>
+          <span tal:condition="request/msg|nothing" tal:replace="request/msg" />
+          <p i18n:translate=""><span i18n:name="link"><a href="password_form" i18n:translate="">Click here</a></span> to change your password.</p>
+          <form action="personalize" method="post" tal:attributes="action string:${purl}/personalize">
+            <table class="TwoColumnForm" cellspacing="0">
+              <tr>
+                <th i18n:translate="">Given Name</th>
+                <td>
+                  <input type="text" name="given_name" tal:attributes="value member/given_name|nothing" />
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Name</th>
+                <td>
+                  <input type="text" name="name" value="" tal:attributes="value member/name|nothing" />
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Email address</th>
+                <td>
+                  <input type="text" name="email" value="" tal:attributes="value member/email|nothing" />
+                </td>
+              </tr>
+              <tr>
+                <td colspan="2" style="text-align:center">
+                  <h3 i18n:translate="">Billing informations</h3>
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Address</th>
+                <td>
+                  <textarea name="billing_address" tal:content="member/billing_address"
+                            cols="30" rows="1" style="width:auto"></textarea>
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">City</th>
+                <td>
+                  <input type="text" name="billing_city" size="35" tal:attributes="value member/billing_city"/>
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Zip code</th>
+                <td>
+                  <input type="text" name="billing_zipcode" size="5" tal:attributes="value member/billing_zipcode"/>
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Country</th>
+                <td>
+                  <select name="country"
+                          tal:define="countries python:modules['Products.iso_3166_1'].fr.countries"
+                          i18n:domain="iso_3166_1">
+                    <option tal:repeat="c countries" tal:attributes="value python:c[0]; selected python:c[0]==member.country" tal:content="python:c[0]" i18n:translate=""></option>
+                  </select>
+                </td>
+              </tr>
+              <tr>
+                <th i18n:translate="">Phone</th>
+                <td>
+                  <input type="text" name="phone" tal:attributes="value member/phone"/>
+                </td>
+              </tr>
+              <tr>
+                <td><br /></td>
+                <td>
+                  <br />
+                  <input type="submit" value="Change" i18n:attributes="value" />
+                </td>
+              </tr>
+            </table>
+          </form>
+        </div>
+        <!-- class="Desktop" -->
+      </div>
+      <!-- tal:define="mtool" -->
+    </div>
+    <!-- metal:fill-slot="main" -->
+  </body>
+
+</html>
\ No newline at end of file
diff --git a/skins/photoprint_templates_edit_form.py b/skins/photoprint_templates_edit_form.py
new file mode 100755 (executable)
index 0000000..94851be
--- /dev/null
@@ -0,0 +1,73 @@
+##parameters=addTemplate='', edit='', deleteOptionContainer='', createOptionsContainer=''
+from Products.CMFCore.utils import getToolByName
+from Products.photoprint.utils import translate
+_ = lambda msg : translate(msg, context)
+
+utool = getToolByName(context, 'portal_url')
+pptool = getToolByName(context, 'portal_photo_print')
+form = context.REQUEST.form.copy()
+
+if addTemplate:
+       context.REQUEST.RESPONSE.setHeader('Content-Type', 'text/xml;;charset=utf-8');
+       fg = form.get
+
+       try :
+               orderTemplate = pptool.addPrintOrderTemplate( context
+                                                                                                       , title=fg('title')
+                                                                                                       , description=fg('description')
+                                                                                                       , productReference=fg('productReference')
+                                                                                                       , maxCopies=fg('maxCopies')
+                                                                                                       , price=fg('price')
+                                                                                                       , VATRate=fg('VATRate')
+                                                                                                       )
+               templateOptions = {}
+               templateOptions['o'] = orderTemplate
+               classRowName = 'odd'
+               if context.printingOptions.getObjectPosition(orderTemplate.getId()) % 2 == 0 :
+                       classRowName = 'even'
+               templateOptions['classRowName'] = classRowName
+               templateOptions['portal_url'] = utool()
+               widget = context.get_photo_print_order_template(**templateOptions).encode('utf-8').strip()
+               return '<computedField type="added">%s</computedField>' % widget
+               
+       except ValueError, e:
+               return '<error>%s</error>' % _(e)
+
+elif edit:
+       context.REQUEST.RESPONSE.setHeader('Content-Type', 'text/xml;;charset=utf-8');
+       id = form.pop('id')
+       try :
+               orderTemplate = pptool.editPrintOrderTemplate(context, id, **form)
+
+               templateOptions = {}
+               templateOptions['o'] = orderTemplate
+               classRowName = 'odd'
+               if context.printingOptions.getObjectPosition(orderTemplate.getId()) % 2 == 0 :
+                       classRowName = 'even'
+               templateOptions['classRowName'] = classRowName
+               templateOptions['portal_url'] = utool()
+               widget = context.get_photo_print_order_template(**templateOptions).encode('utf-8').strip()
+               return '<computedField type="edited">%s</computedField>' % widget
+       except ValueError, e :
+               return '<error>%s</error>' % _(e)
+
+elif createOptionsContainer :
+       pptool.createPrintingOptionsContainer(context)
+       context.setStatus(True, _('Printing options added.'))
+
+elif deleteOptionContainer :   
+       pptool.deletePrintingOptionsContainer(context)
+       context.setStatus(True, _('Printing options deleted.'))
+
+
+options = {}
+options['pptool'] = pptool
+options['hasPO'] = pptool.hasPrintingOptions(context)
+pOptionsSrc = pptool.getPrintingOptionsSrc(context)
+if pOptionsSrc :
+       pourl = pOptionsSrc.getActionInfo('object/printing_settings')['url']
+else :
+       pourl = None
+options['pourl'] = pourl
+
+return context.photoprint_templates_edit_template(**options)
diff --git a/skins/photoprint_templates_edit_template.pt b/skins/photoprint_templates_edit_template.pt
new file mode 100644 (file)
index 0000000..ee0d409
--- /dev/null
@@ -0,0 +1,170 @@
+<html metal:use-macro="here/main_template/macros/master"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  <head>
+    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
+    <metal:block metal:fill-slot="javascript_head_slot">
+      <script type="text/javascript" tal:attributes="src here/widget_form_manager.js/absolute_url"></script>
+    </metal:block>
+  </head>
+  <body i18n:domain="photoprint">
+    <div metal:fill-slot="main"
+         tal:define="pptool nocall:options/pptool;
+                     hasPO python:pptool.hasPrintingOptions(here);
+                     pourl options/pourl">
+      
+      <form tal:attributes="action string:${here/absolute_url}/photoprint_templates_edit_form">
+        <div tal:condition="not:hasPO">
+          <span i18n:translate="" tal:omit-tag="">
+            No printing options are defined at this level.
+          </span><br/>
+          <span tal:condition="pourl" tal:omit-tag="">
+            <span i18n:translate="" tal:omit-tag="">
+              The printing options that apply here are defined above:
+            </span><br/>
+            <a tal:attributes="href pourl" tal:content="pourl"></a><br/>
+          </span>
+          <input type="submit" name="createOptionsContainer"
+                 value="Define printing options" i18n:attributes="value" />
+        </div>
+      </form>
+      
+      <div id="print_order_templates_editing_area" style="padding-top:1em">
+        <form tal:attributes="action string:${here/absolute_url}/photoprint_templates_edit_form"
+              id="print_order_templates_edit_form"
+              tal:condition="hasPO">
+          <table id="print_order_templates_data_area" class="listing" cellspacing="0" style="width:auto">
+            <thead>
+              <tr>
+                <th><br/></th>
+                <th i18n:translate="">Title</th>
+                <th i18n:translate="">Reference</th>
+                <th i18n:translate="">Max. number of copies</th>
+                <th i18n:translate="">Price</th>
+                <th i18n:translate="">VAT (%)</th>
+                <th><br/></th>
+            </thead>
+            <tal:block tal:repeat="o python:pptool.getPrintOrderOptionsContainerFor(here).objectValues()">
+              <tal:block tal:define="classRowName python:repeat['o'].odd() and 'odd' or 'even'">
+                <tbody metal:define-macro="print_order_template_row">
+                  <tr tal:attributes="class classRowName">
+                    <td>
+                      <a tal:attributes="href string:${o/absolute_url}/delete_object" name="rm">
+                        <img tal:attributes="src string:${portal_url}/rm.png;
+                                             onmouseover string:this.src='${portal_url}/rm_over.png';
+                                             onmouseout string:this.src='${portal_url}/rm.png'"
+                                             width="11" height="11" border="0" />
+                      </a>
+                    </td>
+                    <td tal:content="o/title">title</td>
+                    <td tal:content="o/productReference">reference</td>
+                    <td tal:content="o/maxCopies"></td>
+                    <td tal:content="o/price/taxed">price</td>
+                    <td tal:content="o/price/vat">vat</td>
+                    <td rowspan="2">
+                      <a tal:attributes="href string:${o/absolute_url}/formWidgetData" name="edit">
+                        <img tal:attributes="src string:${portal_url}/pencil.png"
+                                             width="16" height="16" border="0" />
+                      </a>
+                    </td>
+                  </tr>
+                  <tr tal:attributes="class classRowName">
+                    <td colspan="2"><br /></td>
+                    <td colspan="4"><em tal:content="o/description">bla</em></td>
+                  </tr>
+                </tbody>
+              </tal:block>
+            </tal:block>
+          </table>
+          <a href="." title="Add print order template" name="add" i18n:attributes="title">
+            <img tal:attributes="src string:$portal_url/add.png"
+                 alt="Add print order template" width="11" height="11" border="0"
+                 style="margin-top:0.5em"
+                 i18n:attributes="alt"/>
+          </a>
+          <br/>
+        </form>
+      </div>
+      <form tal:attributes="action string:${here/absolute_url}/photoprint_templates_edit_form"
+            tal:condition="hasPO">
+        <input type="submit" name="deleteOptionContainer" style="margin-top:3em"
+               value="Delete options defined at this level" i18n:attributes="value"/>
+      </form>
+      
+      <!-- form widgets -->
+      <div class="hidden">
+        <div id="order_template_add_widget">
+          <table class="TwoColumnForm" metal:define-macro="order_template_form_widget">
+            <tr>
+              <th i18n:translate="">Title</th>
+              <td colspan="3">
+                <input type="text" name="title" size="30" />
+              </td>
+            </tr>
+            <tr>
+              <th i18n:translate="">Description</th>
+              <td colspan="3">
+                <textarea name="description" rows="5"></textarea>
+              </td>
+            </tr>
+            <tr>
+              <th i18n:translate="">Product reference</th>
+              <td colspan="3">
+                <input type="text" name="productReference" size="30" />
+              </td>
+            </tr>
+            <tr>
+              <th i18n:translate="">Max. number of copies</th>
+              <td>
+                <input type="text" name="maxCopies" size="3"/>
+                <dl class="FieldHelp">
+                  <dd i18n:translate="max_copies_field_help">The 0 value means unlimited</dd>
+                </dl>
+              </td>
+            </tr>
+            <tr>
+              <th i18n:translate="">Price</th>
+              <td>
+                <input type="text" name="price" size="3" />
+              </td>
+              <th i18n:translate="">VAT (%)</th>
+              <td>
+                <input type="text" name="VATRate" size="3"/>
+              </td>
+            </tr>
+            <tr>
+              <td><br /></td>
+              <td colspan="3">
+                <metal:block  metal:define-slot="buttons">
+                  <input type="submit" name="addTemplate" value="Add" i18n:attributes="value" style="float:left"/>
+                </metal:block>
+                <input type="submit" name="cancel" value="Cancel" i18n:attributes="value" style="float:right"/>
+              </td>
+          </table>
+        </div>
+
+        <div id="order_template_edit_widget">
+          <metal:block metal:use-macro="template/macros/order_template_form_widget">
+            <metal:block metal:fill-slot="buttons">
+              <input type="submit" name="edit" value="Save" i18n:attributes="value" style="float:left"/>
+              <input type="hidden" name="id" value=""/>
+            </metal:block>
+          </metal:block>
+        </div>
+      </div>
+      
+      <script type="text/javascript">
+      // <!--
+                 new WidgetBasedFormManager( {'add'  : document.getElementById('order_template_add_widget')
+                                             ,'edit' : document.getElementById('order_template_edit_widget')
+                                             }
+                                                                               , document.getElementById('print_order_templates_editing_area')
+                                                                               , document.getElementById('print_order_templates_data_area')
+                                                                                 , 7 // number of cols
+                                                                               );
+      // -->
+      </script>
+    </div>
+  </body>
+</html>
diff --git a/tool.gif b/tool.gif
new file mode 100644 (file)
index 0000000..a680450
Binary files /dev/null and b/tool.gif differ
diff --git a/tool.py b/tool.py
new file mode 100755 (executable)
index 0000000..7bc3ec7
--- /dev/null
+++ b/tool.py
@@ -0,0 +1,284 @@
+# -*- coding: utf-8 -*-
+############################################################
+# Copyright © 2009 Benoît PIN <pinbe@luxia.fr>             #
+# Cliché - http://luxia.fr                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+"""
+Photo print tool. Used to link photo to print orders.
+
+$Id: tool.py 1157 2009-06-13 07:14:39Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/tool.py $
+"""
+
+from AccessControl import ClassSecurityInfo
+from AccessControl.requestmethod import postonly
+from Acquisition import aq_base, aq_inner
+from Globals import InitializeClass
+from OFS.OrderedFolder import OrderedFolder
+from Products.CMFCore.utils import UniqueObject, getToolByName
+from permissions import ManagePrintOrderTemplate
+from price import Price
+from utils import Message as _
+from Products.Plinn.utils import makeValidId
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from DateTime import DateTime
+from Products.Plinn.utils import _sudo
+
+
+PRINTING_OPTIONS_ID = 'printingOptions'
+COPIES_COUNTERS = '_copies_counters'
+SOLD_OUT = 'SOLD_OUT'
+
+
+class PhotoPrintTool(UniqueObject, OrderedFolder) :
+       """
+       Provide utilities to configure possible printing works
+       over photo of the portal.
+       """
+       
+       id = 'portal_photo_print'
+       meta_type = 'Photo print tool'
+       
+       security = ClassSecurityInfo()
+       
+       incomingOrderPath = 'commandes'
+       no_shipping_threshold = 150
+       shipping = 6.0
+       shipping_vat = 0.196
+       store_name = ''
+       _order_counter = 0
+       _transaction_id_counter = 0
+       
+       _properties = OrderedFolder._properties + (
+               {'id' : 'incomingOrderPath',            'type' : 'string',      'mode' : 'w'},
+               {'id' : 'no_shipping_threshold',        'type' : 'int',         'mode' : 'w'},
+               {'id' : 'shipping',                                     'type' : 'float',       'mode' : 'w'},
+               {'id' : 'shipping_vat',                         'type' : 'float',       'mode' : 'w'},
+               {'id' : 'store_name',                           'type' : 'string',      'mode' : 'w'}
+               )
+       
+       
+       security.declarePublic('getPrintingOptionsFor')
+       def getPrintingOptionsFor(self, ob) :
+               "returns printing options for the given ob."
+               optionsContainer = getattr(aq_inner(ob), PRINTING_OPTIONS_ID, None)
+               if optionsContainer is None :
+                       return None
+               
+               counters = self.getCountersFor(ob)
+               if counters.get(SOLD_OUT) :
+                       return None
+               
+               options = []
+               for o in optionsContainer.objectValues() :
+                       if o.maxCopies == 0 or \
+                               counters.get(o.productReference, 0) < o.maxCopies :
+                               options.append(o)
+               
+               return options
+       
+       security.declarePublic('getPrintingOptionsContainerFor')
+       def getPrintingOptionsContainerFor(self, ob):
+               """getPrintingOptionsContainerFor
+               """
+               return getattr(ob, PRINTING_OPTIONS_ID, None)
+       
+       security.declarePrivate('getCountersFor')
+       def getCountersFor(self, ob):
+               if hasattr(ob.aq_self, COPIES_COUNTERS) :
+                       return getattr(ob, COPIES_COUNTERS)
+               else :
+                       return {}
+       
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'createPrintingOptionsContainer')
+       def createPrintingOptionsContainer(self, ob):
+               container = PrintingOptionsContainer()
+               setattr(ob, PRINTING_OPTIONS_ID, container)
+               return getattr(ob, PRINTING_OPTIONS_ID)
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'deletePrintingOptionsContainer')
+       def deletePrintingOptionsContainer(self, ob):
+               if not self.hasPrintingOptions(ob) :
+                       raise ValueError( _('No printing options found at %r') % ob.absolute_url() )
+               else :
+                       delattr(ob, PRINTING_OPTIONS_ID)
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'hasPrintingOptions')
+       def hasPrintingOptions(self, ob):
+               """ return boolean that instruct if there's printing
+                       options especially defined on ob 
+               """
+               return hasattr(aq_base(ob), PRINTING_OPTIONS_ID)
+       
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'getPrintingOptionsSrc')
+       def getPrintingOptionsSrc(self, ob) :
+               optionsContainer = getattr(ob, PRINTING_OPTIONS_ID, None)
+               if optionsContainer is None :
+                       return None
+               src = optionsContainer.aq_inner.aq_parent
+               return src
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'getPrintOrderOptionsContainerFor')
+       def getPrintOrderOptionsContainerFor(self, ob) :
+               """
+               returns the printing options container or None.
+               """
+               if hasattr(aq_base(ob), PRINTING_OPTIONS_ID) :
+                       return getattr(ob, PRINTING_OPTIONS_ID)
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'addPrintOrderTemplate')
+       @postonly
+       def addPrintOrderTemplate(self
+                                                       , ob
+                                                       , title=''
+                                                       , description=''
+                                                       , productReference=''
+                                                       , maxCopies=0
+                                                       , price=0
+                                                       , VATRate=0
+                                                       , REQUEST=None):
+               
+               title, maxCopies, price, VATRate = PhotoPrintTool._ckeckTemplateParams(title, maxCopies, price,  VATRate)
+               
+               container = getattr(ob, PRINTING_OPTIONS_ID)
+               
+               id = makeValidId(container, title)
+               
+               factory = getUtility(IFactory, 'photoprint.order_template')
+               orderTemplate = factory( id
+                                                          , title=title
+                                                          , description=description
+                                                          , productReference=productReference
+                                                          , maxCopies=maxCopies
+                                                          , price=price
+                                                          , VATRate=VATRate
+                                                          )
+               container._setObject(id, orderTemplate)
+               return orderTemplate.__of__(container)
+               
+       
+       security.declareProtected(ManagePrintOrderTemplate, 'editPrintOrderTemplate')
+       @postonly
+       def editPrintOrderTemplate(self, ob, id, REQUEST=None, **kw):
+               container = self.getPrintingOptionsContainerFor(ob)
+               orderTemplate = getattr(container, id)
+
+               g = kw.get
+               title, description, productReference, maxCopies, price, VATRate = \
+                       g('title', ''), g('description', ''), g('productReference'), g('maxCopies',0), g('price',0), g('VATRate', 0)
+               title, maxCopies, price, VATRate = PhotoPrintTool._ckeckTemplateParams(title, maxCopies, price, VATRate)
+               
+               orderTemplate.edit( title=title
+                                                 , description=description
+                                                 , productReference=productReference
+                                                 , maxCopies = maxCopies
+                                                 , price=price
+                                                 , VATRate=VATRate)
+               
+               return orderTemplate
+       
+       @staticmethod
+       def _ckeckTemplateParams(title, maxCopies, price, VATRate) :
+               title = title.strip()
+               
+               if not title :
+                       raise ValueError(_(u'You must enter a title.'))
+               try :
+                       maxCopies = int(maxCopies)
+               except ValueError :
+                       raise ValueError(_(u'You must enter an integer number\nfor the maximum number of copies.'))
+               if maxCopies < 0 :
+                       raise ValueError(_(u'You must enter a positive value\nfor the maximum number of copies.'))
+               try :
+                       price = float(price.replace(',', '.'))
+               except ValueError :
+                       raise ValueError(_(u'You must enter a numeric value for the price.'))
+       
+               try :
+                       VATRate = float(VATRate.replace(',', '.')) / 100
+               except ValueError :
+                       raise ValueError(_(u'You must enter a numeric value for the VAT rate.'))
+               
+               return title, maxCopies, price, VATRate
+       
+       security.declarePublic('addPrintOrder')
+       def addPrintOrder(self, cart):
+               utool = getToolByName(self, 'portal_url')
+               portal = utool.getPortalObject()
+               ttool = getToolByName(portal, 'portal_types')
+
+               baseContainer = portal.unrestrictedTraverse(self.getProperty('incomingOrderPath'), None)
+               if baseContainer is None:
+                       parts = self.getProperty('incomingOrderPath').split('/')
+                       baseContainer = portal
+                       for id in parts :
+                               if not hasattr(baseContainer.aq_base, id) :
+                                       id = _sudo(lambda:ttool.constructContent('Order Folder', baseContainer, id))
+                               baseContainer = getattr(baseContainer, id)
+
+               now = DateTime()
+               monthId = now.strftime('%Y-%m')
+               if not hasattr(baseContainer.aq_base, monthId) :
+                       monthId = _sudo(lambda:ttool.constructContent('Order Folder', baseContainer, monthId))
+               
+               container = getattr(baseContainer, monthId)
+
+               self._order_counter += 1
+               id = '%s-%d' % (monthId, self._order_counter)
+               id = container.invokeFactory('Order', id)
+               ob = getattr(container,id)
+               ob.loadCart(cart)
+               return ob
+       
+       security.declarePublic('getShippingFeesFor')
+       def getShippingFeesFor(self, shippable=None, price=None):
+               # returns Fees
+               # TODO: use adapters
+               # for the moment, shippable objet must provide a 'price' attribute
+               
+               if shippable and price :
+                       raise AttributeError("'shippable' and 'price' are mutually exclusive.")
+               
+               if shippable :
+                       amount = shippable.price.getValues()['taxed']
+               else :
+                       amount = price.getValues()['taxed']
+               
+               threshold = self.getProperty('no_shipping_threshold')
+
+               if amount < threshold :
+                       fees = Price(self.getProperty('shipping')
+                                               , self.getProperty('shipping_vat'))
+               else :
+                       fees = Price(0,0)
+               return fees
+       
+       security.declarePrivate('getNextTransactionId')
+       def getNextTransactionId(self):
+               trid = self._transaction_id_counter
+               trid = (trid + 1) % 1000000
+               self._transaction_id_counter = trid
+               trid = str(trid).zfill(6)
+               return trid
+
+
+InitializeClass(PhotoPrintTool)
+
+
+class PrintingOptionsContainer(OrderedFolder) :
+       meta_type = 'Printing options container'
+       security = ClassSecurityInfo()
+       
+       def __init__(self) :
+               self.id = PRINTING_OPTIONS_ID
+       
+       def __getitem__(self, k) :
+               sd = context.session_data_manager.getSessionData(create = 1)
diff --git a/utils.py b/utils.py
new file mode 100755 (executable)
index 0000000..9f4fb8a
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+############################################################
+# Copyright © 2008  Benoît PIN <benoit.pin@ensmp.fr>       #
+# Plinn - http://plinn.org                                 #
+#                                                          #
+# This program is free software; you can redistribute it   #
+# and/or modify it under the terms of the Creative Commons #
+# "Attribution-Noncommercial 2.0 Generic"                  #
+# http://creativecommons.org/licenses/by-nc/2.0/           #
+############################################################
+"""
+Global utilities
+
+$Id: utils.py 651 2009-02-04 15:38:20Z pin $
+$URL: http://svn.luxia.fr/svn/labo/projects/zope/photoprint/trunk/utils.py $
+"""
+
+from AccessControl import ModuleSecurityInfo
+from Products.PageTemplates.GlobalTranslationService import getGlobalTranslationService
+from zope.i18nmessageid import MessageFactory
+
+security = ModuleSecurityInfo('Products.photoprint.utils')
+
+security.declarePublic('translate')
+def translate(message, context):
+       """ Translate i18n message.
+       """
+       GTS = getGlobalTranslationService()
+       if isinstance(message, Exception):
+               try:
+                       message = message[0]
+               except (TypeError, IndexError):
+                       pass
+       return GTS.translate('photoprint', message, context=context)
+
+security.declarePublic('Message')
+Message = _ = MessageFactory('photoprint')