Oubli de mot de passe : utilisation du formulaire de réinitialisation au lieu de...
[photoprint.git] / order.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2009-2013 Benoît PIN <benoit.pin@ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """
21 Print order classes
22
23
24
25 """
26
27 from Globals import InitializeClass, PersistentMapping, Persistent
28 from Acquisition import Implicit
29 from AccessControl import ClassSecurityInfo
30 from AccessControl.requestmethod import postonly
31 from zope.interface import implements
32 from zope.component.factory import Factory
33 from persistent.list import PersistentList
34 from OFS.SimpleItem import SimpleItem
35 from ZTUtils import make_query
36 from DateTime import DateTime
37 from Products.CMFCore.PortalContent import PortalContent
38 from Products.CMFCore.permissions import ModifyPortalContent, View, ManagePortal
39 from Products.CMFCore.utils import getToolByName, getUtilityByInterfaceName
40 from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
41 from Products.Plinn.utils import getPreferredLanguages
42 from interfaces import IPrintOrderTemplate, IPrintOrder
43 from permissions import ManagePrintOrderTemplate, ManagePrintOrders
44 from price import Price
45 from xml.dom.minidom import Document
46 from tool import COPIES_COUNTERS
47 from App.config import getConfiguration
48 from paypal.interface import PayPalInterface
49 from logging import getLogger
50 console = getLogger('Products.photoprint.order')
51
52
53 def getPayPalConfig() :
54 zopeConf = getConfiguration()
55 try :
56 conf = zopeConf.product_config['photoprint']
57 except KeyError :
58 EnvironmentError("No photoprint configuration found in Zope environment.")
59
60 ppconf = {'API_ENVIRONMENT' : conf['paypal_api_environment'],
61 'API_USERNAME' : conf['paypal_username'],
62 'API_PASSWORD' : conf['paypal_password'],
63 'API_SIGNATURE' : conf['paypal_signature']}
64
65 return ppconf
66
67
68 class PrintOrderTemplate(SimpleItem) :
69 """
70 predefined print order
71 """
72 implements(IPrintOrderTemplate)
73
74 security = ClassSecurityInfo()
75
76 def __init__(self
77 , id
78 , title=''
79 , description=''
80 , productReference=''
81 , maxCopies=0
82 , price=0
83 , VATRate=0) :
84 self.id = id
85 self.title = title
86 self.description = description
87 self.productReference = productReference
88 self.maxCopies = maxCopies # 0 means unlimited
89 self.price = Price(price, VATRate)
90
91 security.declareProtected(ManagePrintOrderTemplate, 'edit')
92 def edit( self
93 , title=''
94 , description=''
95 , productReference=''
96 , maxCopies=0
97 , price=0
98 , VATRate=0 ) :
99 self.title = title
100 self.description = description
101 self.productReference = productReference
102 self.maxCopies = maxCopies
103 self.price = Price(price, VATRate)
104
105 security.declareProtected(ManagePrintOrderTemplate, 'formWidgetData')
106 def formWidgetData(self, REQUEST=None, RESPONSE=None):
107 """formWidgetData documentation
108 """
109 d = Document()
110 d.encoding = 'utf-8'
111 root = d.createElement('formdata')
112 d.appendChild(root)
113
114 def gua(name) :
115 return str(getattr(self, name, '')).decode('utf-8')
116
117 id = d.createElement('id')
118 id.appendChild(d.createTextNode(self.getId()))
119 root.appendChild(id)
120
121 title = d.createElement('title')
122 title.appendChild(d.createTextNode(gua('title')))
123 root.appendChild(title)
124
125 description = d.createElement('description')
126 description.appendChild(d.createTextNode(gua('description')))
127 root.appendChild(description)
128
129 productReference = d.createElement('productReference')
130 productReference.appendChild(d.createTextNode(gua('productReference')))
131 root.appendChild(productReference)
132
133 maxCopies = d.createElement('maxCopies')
134 maxCopies.appendChild(d.createTextNode(str(self.maxCopies)))
135 root.appendChild(maxCopies)
136
137 price = d.createElement('price')
138 price.appendChild(d.createTextNode(str(self.price.taxed)))
139 root.appendChild(price)
140
141 vatrate = d.createElement('VATRate')
142 vatrate.appendChild(d.createTextNode(str(self.price.vat)))
143 root.appendChild(vatrate)
144
145 if RESPONSE is not None :
146 RESPONSE.setHeader('content-type', 'text/xml; charset=utf-8')
147
148 manager = getToolByName(self, 'caching_policy_manager', None)
149 if manager is not None:
150 view_name = 'formWidgetData'
151 headers = manager.getHTTPCachingHeaders(
152 self, view_name, {}
153 )
154
155 for key, value in headers:
156 if key == 'ETag':
157 RESPONSE.setHeader(key, value, literal=1)
158 else:
159 RESPONSE.setHeader(key, value)
160 if headers:
161 RESPONSE.setHeader('X-Cache-Headers-Set-By',
162 'CachingPolicyManager: %s' %
163 '/'.join(manager.getPhysicalPath()))
164
165
166 return d.toxml('utf-8')
167
168
169 InitializeClass(PrintOrderTemplate)
170 PrintOrderTemplateFactory = Factory(PrintOrderTemplate)
171
172 class PrintOrder(PortalContent, DefaultDublinCoreImpl) :
173
174 implements(IPrintOrder)
175 security = ClassSecurityInfo()
176
177 def __init__( self, id) :
178 DefaultDublinCoreImpl.__init__(self)
179 self.id = id
180 self.items = []
181 self.quantity = 0
182 self.price = Price(0, 0)
183 # billing and shipping addresses
184 self.billing = PersistentMapping()
185 self.shipping = PersistentMapping()
186 self.shippingFees = Price(0,0)
187 self._paypalLog = PersistentList()
188
189 @property
190 def amountWithFees(self) :
191 return self.price + self.shippingFees
192
193
194 security.declareProtected(ModifyPortalContent, 'editBilling')
195 def editBilling(self
196 , name
197 , address
198 , city
199 , zipcode
200 , country
201 , phone) :
202 self.billing['name'] = name
203 self.billing['address'] = address
204 self.billing['city'] = city
205 self.billing['zipcode'] = zipcode
206 self.billing['country'] = country
207 self.billing['phone'] = phone
208
209 security.declareProtected(ModifyPortalContent, 'editShipping')
210 def editShipping(self, name, address, city, zipcode, country) :
211 self.shipping['name'] = name
212 self.shipping['address'] = address
213 self.shipping['city'] = city
214 self.shipping['zipcode'] = zipcode
215 self.shipping['country'] = country
216
217 security.declarePrivate('loadCart')
218 def loadCart(self, cart):
219 pptool = getToolByName(self, 'portal_photo_print')
220 uidh = getToolByName(self, 'portal_uidhandler')
221 mtool = getToolByName(self, 'portal_membership')
222
223 items = []
224 for item in cart :
225 photo = uidh.getObject(item['cmf_uid'])
226 pOptions = pptool.getPrintingOptionsContainerFor(photo)
227 template = getattr(pOptions, item['printing_template'])
228
229 reference = template.productReference
230 quantity = item['quantity']
231 uPrice = template.price
232 self.quantity += quantity
233
234 d = {'cmf_uid' : item['cmf_uid']
235 ,'url' : photo.absolute_url()
236 ,'title' : template.title
237 ,'description' : template.description
238 ,'unit_price' : Price(uPrice._taxed, uPrice._rate)
239 ,'quantity' : quantity
240 ,'productReference' : reference
241 }
242 items.append(d)
243 self.price += uPrice * quantity
244 # confirm counters
245 if template.maxCopies :
246 counters = getattr(photo, COPIES_COUNTERS)
247 counters.confirm(reference, quantity)
248
249 self.items = tuple(items)
250
251 member = mtool.getAuthenticatedMember()
252 mg = lambda name : member.getProperty(name, '')
253 billing = {'name' : member.getMemberFullName(nameBefore=0)
254 ,'address' : mg('billing_address')
255 ,'city' : mg('billing_city')
256 ,'zipcode' : mg('billing_zipcode')
257 ,'country' : mg('country')
258 ,'phone' : mg('phone') }
259 self.editBilling(**billing)
260
261 sg = lambda name : cart._shippingInfo.get(name, '')
262 shipping = {'name' : sg('shipping_fullname')
263 ,'address' : sg('shipping_address')
264 ,'city' : sg('shipping_city')
265 ,'zipcode' : sg('shipping_zipcode')
266 ,'country' : sg('shipping_country')}
267 self.editShipping(**shipping)
268
269 self.shippingFees = pptool.getShippingFeesFor(shippable=self)
270
271 cart._confirmed = True
272 cart.pendingOrderPath = self.getPhysicalPath()
273
274 security.declareProtected(ManagePrintOrders, 'resetCopiesCounters')
275 def resetCopiesCounters(self) :
276 pptool = getToolByName(self, 'portal_photo_print')
277 uidh = getToolByName(self, 'portal_uidhandler')
278
279 for item in self.items :
280 photo = uidh.getObject(item['cmf_uid'])
281 counters = getattr(photo, COPIES_COUNTERS, None)
282 if counters :
283 counters.cancel(item['productReference'],
284 item['quantity'])
285
286
287 def _initPayPalInterface(self) :
288 config = getPayPalConfig()
289 config['API_AUTHENTICATION_MODE'] = '3TOKEN'
290 ppi = PayPalInterface(**config)
291 return ppi
292
293
294 @staticmethod
295 def recordifyPPResp(response) :
296 d = {}
297 d['zopeTime'] = DateTime()
298 for k, v in response.raw.iteritems() :
299 if len(v) == 1 :
300 d[k] = v[0]
301 else :
302 d[k] = v
303 return d
304
305 # paypal api
306 security.declareProtected(ModifyPortalContent, 'ppSetExpressCheckout')
307 def ppSetExpressCheckout(self) :
308 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
309 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
310 portal_url = utool()
311 portal = utool.getPortalObject()
312 member = mtool.getAuthenticatedMember()
313
314 options = {'PAYMENTREQUEST_0_CURRENCYCODE' : 'EUR',
315 'PAYMENTREQUEST_0_PAYMENTACTION' : 'Sale',
316 'RETURNURL' : '%s/photoprint_order_confirm' % self.absolute_url(),
317 'CANCELURL' : '%s/photoprint_order_cancel' % self.absolute_url(),
318 'ALLOWNOTE' : 0, # The buyer is unable to enter a note to the merchant.
319 'HDRIMG' : '%s/logo.gif' % portal_url,
320 'EMAIL' : member.getProperty('email'),
321 'SOLUTIONTYPE' : 'Sole', # Buyer does not need to create a PayPal account to check out. This is referred to as PayPal Account Optional.
322 'LANDINGPAGE' : 'Billing', # Non-PayPal account
323 'BRANDNAME' : portal.getProperty('title'),
324 'GIFTMESSAGEENABLE' : 0,
325 'GIFTRECEIPTENABLE' : 0,
326 'BUYEREMAILOPTINENABLE' : 0, # Do not enable buyer to provide email address.
327 'NOSHIPPING' : 1, # PayPal does not display shipping address fields whatsoever.
328 'PAYMENTREQUEST_0_SHIPTONAME' : self.billing['name'],
329 'PAYMENTREQUEST_0_SHIPTOSTREET' : self.billing['address'],
330 'PAYMENTREQUEST_0_SHIPTOCITY' : self.billing['city'],
331 'PAYMENTREQUEST_0_SHIPTOZIP' : self.billing['zipcode'],
332 'PAYMENTREQUEST_0_SHIPTOPHONENUM' : self.billing['phone'],
333 }
334
335 if len(self.items) > 1 :
336 quantitySum = reduce(lambda a, b : a['quantity'] + b['quantity'], self.items)
337 else :
338 quantitySum = self.items[0]['quantity']
339 total = round(self.amountWithFees.getValues()['taxed'], 2)
340
341 options['L_PAYMENTREQUEST_0_NAME0'] = 'Commande realis photo ref. %s' % self.getId()
342 if quantitySum == 1 :
343 options['L_PAYMENTREQUEST_0_DESC0'] = "Commande d'un tirage photographique"
344 else :
345 options['L_PAYMENTREQUEST_0_DESC0'] = 'Commande de %d tirages photographiques' % quantitySum
346 options['L_PAYMENTREQUEST_0_AMT0'] = total
347 options['PAYMENTINFO_0_SHIPPINGAMT'] = round(self.shippingFees.getValues()['taxed'], 2)
348 options['PAYMENTREQUEST_0_AMT'] = total
349
350 ppi = self._initPayPalInterface()
351 response = ppi.set_express_checkout(**options)
352 response = PrintOrder.recordifyPPResp(response)
353 self._paypalLog.append(response)
354 response['url'] = ppi.generate_express_checkout_redirect_url(response['TOKEN'])
355 console.info(options)
356 console.info(response)
357 return response
358
359 security.declarePrivate('ppGetExpressCheckoutDetails')
360 def ppGetExpressCheckoutDetails(self, token) :
361 ppi = self._initPayPalInterface()
362 response = ppi.get_express_checkout_details(TOKEN=token)
363 response = PrintOrder.recordifyPPResp(response)
364 self._paypalLog.append(response)
365 return response
366
367 security.declarePrivate('ppDoExpressCheckoutPayment')
368 def ppDoExpressCheckoutPayment(self, token, payerid, amt) :
369 ppi = self._initPayPalInterface()
370 response = ppi.do_express_checkout_payment(PAYMENTREQUEST_0_PAYMENTACTION='Sale',
371 PAYMENTREQUEST_0_AMT=amt,
372 PAYMENTREQUEST_0_CURRENCYCODE='EUR',
373 TOKEN=token,
374 PAYERID=payerid)
375 response = PrintOrder.recordifyPPResp(response)
376 self._paypalLog.append(response)
377 return response
378
379 security.declareProtected(ModifyPortalContent, 'ppPay')
380 def ppPay(self, token, payerid):
381 # assure le paiement paypal en une passe :
382 # récupération des détails et validation de la transaction.
383
384 wtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
385 wfstate = wtool.getInfoFor(self, 'review_state', 'order_workflow')
386 paid = wfstate == 'paid'
387
388 if not paid :
389 details = self.ppGetExpressCheckoutDetails(token)
390
391 if payerid != details['PAYERID'] :
392 return False
393
394 if details['ACK'] == 'Success' :
395 response = self.ppDoExpressCheckoutPayment(token,
396 payerid,
397 details['AMT'])
398 if response['ACK'] == 'Success' and \
399 response['PAYMENTINFO_0_ACK'] == 'Success' and \
400 response['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Completed' :
401 self.paid = (DateTime(), 'paypal')
402 wtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
403 wtool.doActionFor( self
404 , 'paypal_pay'
405 , wf_id='order_workflow'
406 , comments='Paiement par PayPal')
407 return True
408 return False
409 else :
410 return True
411
412 security.declareProtected(ModifyPortalContent, 'ppCancel')
413 def ppCancel(self, token) :
414 details = self.ppGetExpressCheckoutDetails(token)
415
416 security.declareProtected(ManagePortal, 'getPPLog')
417 def getPPLog(self) :
418 return self._paypalLog
419
420 def getCustomerSummary(self) :
421 ' '
422 return {'quantity':self.quantity,
423 'price':self.price}
424
425
426 InitializeClass(PrintOrder)
427 PrintOrderFactory = Factory(PrintOrder)
428
429
430 class CopiesCounters(Persistent, Implicit) :
431
432 def __init__(self):
433 self._mapping = PersistentMapping()
434
435 def getBrowserId(self):
436 sdm = self.session_data_manager
437 bim = sdm.getBrowserIdManager()
438 browserId = bim.getBrowserId(create=1)
439 return browserId
440
441 def _checkBrowserId(self, browserId) :
442 sdm = self.session_data_manager
443 sd = sdm.getSessionDataByKey(browserId)
444 return not not sd
445
446 def __setitem__(self, reference, count) :
447 if not self._mapping.has_key(reference):
448 self._mapping[reference] = PersistentMapping()
449 self._mapping[reference]['pending'] = PersistentMapping()
450 self._mapping[reference]['confirmed'] = 0
451
452 globalCount = self[reference]
453 delta = count - globalCount
454 bid = self.getBrowserId()
455 if not self._mapping[reference]['pending'].has_key(bid) :
456 self._mapping[reference]['pending'][bid] = delta
457 else :
458 self._mapping[reference]['pending'][bid] += delta
459
460
461 def __getitem__(self, reference) :
462 item = self._mapping[reference]
463 globalCount = item['confirmed']
464
465 for browserId, count in item['pending'].items() :
466 if self._checkBrowserId(browserId) :
467 globalCount += count
468 else :
469 del self._mapping[reference]['pending'][browserId]
470
471 return globalCount
472
473 def get(self, reference, default=0) :
474 if self._mapping.has_key(reference) :
475 return self[reference]
476 else :
477 return default
478
479 def getPendingCounter(self, reference) :
480 bid = self.getBrowserId()
481 if not self._checkBrowserId(bid) :
482 console.warn('BrowserId not found: %s' % bid)
483 return 0
484
485 count = self._mapping[reference]['pending'].get(bid, None)
486 if count is None :
487 console.warn('No pending data found for browserId %s' % bid)
488 return 0
489 else :
490 return count
491
492 def confirm(self, reference, quantity) :
493 pending = self.getPendingCounter(reference)
494 if pending != quantity :
495 console.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending, quantity))
496
497 browserId = self.getBrowserId()
498 if self._mapping[reference]['pending'].has_key(browserId) :
499 del self._mapping[reference]['pending'][browserId]
500 self._mapping[reference]['confirmed'] += quantity
501
502 def cancel(self, reference, quantity) :
503 self._mapping[reference]['confirmed'] -= quantity
504
505 def __str__(self):
506 return str(self._mapping)