1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2009-2013 Benoît PIN <benoit.pin@ensmp.fr> #
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. #
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. #
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 #######################################################################################
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
49 from paypal
.interface
import PayPalInterface
50 paypalAvailable
= True
52 paypalAvailable
= False
53 from logging
import getLogger
54 console
= getLogger('Products.photoprint.order')
57 def getPayPalConfig() :
58 zopeConf
= getConfiguration()
60 conf
= zopeConf
.product_config
['photoprint']
62 EnvironmentError("No photoprint configuration found in Zope environment.")
64 ppconf
= {'API_ENVIRONMENT' : conf
['paypal_api_environment'],
65 'API_USERNAME' : conf
['paypal_username'],
66 'API_PASSWORD' : conf
['paypal_password'],
67 'API_SIGNATURE' : conf
['paypal_signature']}
72 class PrintOrderTemplate(SimpleItem
) :
74 predefined print order
76 implements(IPrintOrderTemplate
)
78 security
= ClassSecurityInfo()
90 self
.description
= description
91 self
.productReference
= productReference
92 self
.maxCopies
= maxCopies
# 0 means unlimited
93 self
.price
= Price(price
, VATRate
)
95 security
.declareProtected(ManagePrintOrderTemplate
, 'edit')
104 self
.description
= description
105 self
.productReference
= productReference
106 self
.maxCopies
= maxCopies
107 self
.price
= Price(price
, VATRate
)
109 security
.declareProtected(ManagePrintOrderTemplate
, 'formWidgetData')
110 def formWidgetData(self
, REQUEST
=None, RESPONSE
=None):
111 """formWidgetData documentation
115 root
= d
.createElement('formdata')
119 return str(getattr(self
, name
, '')).decode('utf-8')
121 id = d
.createElement('id')
122 id.appendChild(d
.createTextNode(self
.getId()))
125 title
= d
.createElement('title')
126 title
.appendChild(d
.createTextNode(gua('title')))
127 root
.appendChild(title
)
129 description
= d
.createElement('description')
130 description
.appendChild(d
.createTextNode(gua('description')))
131 root
.appendChild(description
)
133 productReference
= d
.createElement('productReference')
134 productReference
.appendChild(d
.createTextNode(gua('productReference')))
135 root
.appendChild(productReference
)
137 maxCopies
= d
.createElement('maxCopies')
138 maxCopies
.appendChild(d
.createTextNode(str(self
.maxCopies
)))
139 root
.appendChild(maxCopies
)
141 price
= d
.createElement('price')
142 price
.appendChild(d
.createTextNode(str(self
.price
.taxed
)))
143 root
.appendChild(price
)
145 vatrate
= d
.createElement('VATRate')
146 vatrate
.appendChild(d
.createTextNode(str(self
.price
.vat
)))
147 root
.appendChild(vatrate
)
149 if RESPONSE
is not None :
150 RESPONSE
.setHeader('content-type', 'text/xml; charset=utf-8')
152 manager
= getToolByName(self
, 'caching_policy_manager', None)
153 if manager
is not None:
154 view_name
= 'formWidgetData'
155 headers
= manager
.getHTTPCachingHeaders(
159 for key
, value
in headers
:
161 RESPONSE
.setHeader(key
, value
, literal
=1)
163 RESPONSE
.setHeader(key
, value
)
165 RESPONSE
.setHeader('X-Cache-Headers-Set-By',
166 'CachingPolicyManager: %s' %
167 '/'.join(manager
.getPhysicalPath()))
170 return d
.toxml('utf-8')
173 InitializeClass(PrintOrderTemplate
)
174 PrintOrderTemplateFactory
= Factory(PrintOrderTemplate
)
176 class PrintOrder(PortalContent
, DefaultDublinCoreImpl
) :
178 implements(IPrintOrder
)
179 security
= ClassSecurityInfo()
181 def __init__( self
, id) :
182 DefaultDublinCoreImpl
.__init
__(self
)
186 self
.price
= Price(0, 0)
187 # billing and shipping addresses
188 self
.billing
= PersistentMapping()
189 self
.shipping
= PersistentMapping()
190 self
.shippingFees
= Price(0,0)
191 self
._paypalLog
= PersistentList()
194 def amountWithFees(self
) :
195 return self
.price
+ self
.shippingFees
198 security
.declareProtected(ModifyPortalContent
, 'editBilling')
206 self
.billing
['name'] = name
207 self
.billing
['address'] = address
208 self
.billing
['city'] = city
209 self
.billing
['zipcode'] = zipcode
210 self
.billing
['country'] = country
211 self
.billing
['phone'] = phone
213 security
.declareProtected(ModifyPortalContent
, 'editShipping')
214 def editShipping(self
, name
, address
, city
, zipcode
, country
) :
215 self
.shipping
['name'] = name
216 self
.shipping
['address'] = address
217 self
.shipping
['city'] = city
218 self
.shipping
['zipcode'] = zipcode
219 self
.shipping
['country'] = country
221 security
.declarePrivate('loadCart')
222 def loadCart(self
, cart
):
223 pptool
= getToolByName(self
, 'portal_photo_print')
224 uidh
= getToolByName(self
, 'portal_uidhandler')
225 mtool
= getToolByName(self
, 'portal_membership')
229 photo
= uidh
.getObject(item
['cmf_uid'])
230 pOptions
= pptool
.getPrintingOptionsContainerFor(photo
)
231 template
= getattr(pOptions
, item
['printing_template'])
233 reference
= template
.productReference
234 quantity
= item
['quantity']
235 uPrice
= template
.price
236 self
.quantity
+= quantity
238 d
= {'cmf_uid' : item
['cmf_uid']
239 ,'url' : photo
.absolute_url()
240 ,'title' : template
.title
241 ,'description' : template
.description
242 ,'unit_price' : Price(uPrice
._taxed
, uPrice
._rate
)
243 ,'quantity' : quantity
244 ,'productReference' : reference
247 self
.price
+= uPrice
* quantity
249 if template
.maxCopies
:
250 counters
= getattr(photo
, COPIES_COUNTERS
)
251 counters
.confirm(reference
, quantity
)
253 self
.items
= tuple(items
)
255 member
= mtool
.getAuthenticatedMember()
256 mg
= lambda name
: member
.getProperty(name
, '')
257 billing
= {'name' : member
.getMemberFullName(nameBefore
=0)
258 ,'address' : mg('billing_address')
259 ,'city' : mg('billing_city')
260 ,'zipcode' : mg('billing_zipcode')
261 ,'country' : mg('country')
262 ,'phone' : mg('phone') }
263 self
.editBilling(**billing
)
265 sg
= lambda name
: cart
._shippingInfo
.get(name
, '')
266 shipping
= {'name' : sg('shipping_fullname')
267 ,'address' : sg('shipping_address')
268 ,'city' : sg('shipping_city')
269 ,'zipcode' : sg('shipping_zipcode')
270 ,'country' : sg('shipping_country')}
271 self
.editShipping(**shipping
)
273 self
.shippingFees
= pptool
.getShippingFeesFor(shippable
=self
)
275 cart
._confirmed
= True
276 cart
.pendingOrderPath
= self
.getPhysicalPath()
278 security
.declareProtected(ManagePrintOrders
, 'resetCopiesCounters')
279 def resetCopiesCounters(self
) :
280 pptool
= getToolByName(self
, 'portal_photo_print')
281 uidh
= getToolByName(self
, 'portal_uidhandler')
283 for item
in self
.items
:
284 photo
= uidh
.getObject(item
['cmf_uid'])
285 counters
= getattr(photo
, COPIES_COUNTERS
, None)
287 counters
.cancel(item
['productReference'],
291 def _initPayPalInterface(self
) :
292 config
= getPayPalConfig()
293 config
['API_AUTHENTICATION_MODE'] = '3TOKEN'
294 ppi
= PayPalInterface(**config
)
299 def recordifyPPResp(response
) :
301 d
['zopeTime'] = DateTime()
302 for k
, v
in response
.raw
.iteritems() :
310 security
.declareProtected(ModifyPortalContent
, 'ppSetExpressCheckout')
311 def ppSetExpressCheckout(self
) :
312 utool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
313 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
315 portal
= utool
.getPortalObject()
316 member
= mtool
.getAuthenticatedMember()
318 options
= {'PAYMENTREQUEST_0_CURRENCYCODE' : 'EUR',
319 'PAYMENTREQUEST_0_PAYMENTACTION' : 'Sale',
320 'RETURNURL' : '%s/photoprint_order_confirm' % self
.absolute_url(),
321 'CANCELURL' : '%s/photoprint_order_cancel' % self
.absolute_url(),
322 'ALLOWNOTE' : 0, # The buyer is unable to enter a note to the merchant.
323 'HDRIMG' : '%s/logo.gif' % portal_url
,
324 'EMAIL' : member
.getProperty('email'),
325 'SOLUTIONTYPE' : 'Sole', # Buyer does not need to create a PayPal account to check out. This is referred to as PayPal Account Optional.
326 'LANDINGPAGE' : 'Billing', # Non-PayPal account
327 'BRANDNAME' : portal
.getProperty('title'),
328 'GIFTMESSAGEENABLE' : 0,
329 'GIFTRECEIPTENABLE' : 0,
330 'BUYEREMAILOPTINENABLE' : 0, # Do not enable buyer to provide email address.
331 'NOSHIPPING' : 1, # PayPal does not display shipping address fields whatsoever.
332 'PAYMENTREQUEST_0_SHIPTONAME' : self
.billing
['name'],
333 'PAYMENTREQUEST_0_SHIPTOSTREET' : self
.billing
['address'],
334 'PAYMENTREQUEST_0_SHIPTOCITY' : self
.billing
['city'],
335 'PAYMENTREQUEST_0_SHIPTOZIP' : self
.billing
['zipcode'],
336 'PAYMENTREQUEST_0_SHIPTOPHONENUM' : self
.billing
['phone'],
339 if len(self
.items
) > 1 :
340 quantitySum
= reduce(lambda a
, b
: a
['quantity'] + b
['quantity'], self
.items
)
342 quantitySum
= self
.items
[0]['quantity']
343 total
= round(self
.amountWithFees
.getValues()['taxed'], 2)
345 options
['L_PAYMENTREQUEST_0_NAME0'] = 'Commande realis photo ref. %s' % self
.getId()
346 if quantitySum
== 1 :
347 options
['L_PAYMENTREQUEST_0_DESC0'] = "Commande d'un tirage photographique"
349 options
['L_PAYMENTREQUEST_0_DESC0'] = 'Commande de %d tirages photographiques' % quantitySum
350 options
['L_PAYMENTREQUEST_0_AMT0'] = total
351 options
['PAYMENTINFO_0_SHIPPINGAMT'] = round(self
.shippingFees
.getValues()['taxed'], 2)
352 options
['PAYMENTREQUEST_0_AMT'] = total
354 ppi
= self
._initPayPalInterface
()
355 response
= ppi
.set_express_checkout(**options
)
356 response
= PrintOrder
.recordifyPPResp(response
)
357 self
._paypalLog
.append(response
)
358 response
['url'] = ppi
.generate_express_checkout_redirect_url(response
['TOKEN'])
359 console
.info(options
)
360 console
.info(response
)
363 security
.declarePrivate('ppGetExpressCheckoutDetails')
364 def ppGetExpressCheckoutDetails(self
, token
) :
365 ppi
= self
._initPayPalInterface
()
366 response
= ppi
.get_express_checkout_details(TOKEN
=token
)
367 response
= PrintOrder
.recordifyPPResp(response
)
368 self
._paypalLog
.append(response
)
371 security
.declarePrivate('ppDoExpressCheckoutPayment')
372 def ppDoExpressCheckoutPayment(self
, token
, payerid
, amt
) :
373 ppi
= self
._initPayPalInterface
()
374 response
= ppi
.do_express_checkout_payment(PAYMENTREQUEST_0_PAYMENTACTION
='Sale',
375 PAYMENTREQUEST_0_AMT
=amt
,
376 PAYMENTREQUEST_0_CURRENCYCODE
='EUR',
379 response
= PrintOrder
.recordifyPPResp(response
)
380 self
._paypalLog
.append(response
)
383 security
.declareProtected(ModifyPortalContent
, 'ppPay')
384 def ppPay(self
, token
, payerid
):
385 # assure le paiement paypal en une passe :
386 # récupération des détails et validation de la transaction.
388 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
389 wfstate
= wtool
.getInfoFor(self
, 'review_state', 'order_workflow')
390 paid
= wfstate
== 'paid'
393 details
= self
.ppGetExpressCheckoutDetails(token
)
395 if payerid
!= details
['PAYERID'] :
398 if details
['ACK'] == 'Success' :
399 response
= self
.ppDoExpressCheckoutPayment(token
,
402 if response
['ACK'] == 'Success' and \
403 response
['PAYMENTINFO_0_ACK'] == 'Success' and \
404 response
['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Completed' :
405 self
.paid
= (DateTime(), 'paypal')
406 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
407 wtool
.doActionFor( self
409 , wf_id
='order_workflow'
410 , comments
='Paiement par PayPal')
416 security
.declareProtected(ModifyPortalContent
, 'ppCancel')
417 def ppCancel(self
, token
) :
418 details
= self
.ppGetExpressCheckoutDetails(token
)
420 security
.declareProtected(ManagePortal
, 'getPPLog')
422 return self
._paypalLog
424 def getCustomerSummary(self
) :
426 return {'quantity':self
.quantity
,
430 InitializeClass(PrintOrder
)
431 PrintOrderFactory
= Factory(PrintOrder
)
434 class CopiesCounters(Persistent
, Implicit
) :
437 self
._mapping
= PersistentMapping()
439 def getBrowserId(self
):
440 sdm
= self
.session_data_manager
441 bim
= sdm
.getBrowserIdManager()
442 browserId
= bim
.getBrowserId(create
=1)
445 def _checkBrowserId(self
, browserId
) :
446 sdm
= self
.session_data_manager
447 sd
= sdm
.getSessionDataByKey(browserId
)
450 def __setitem__(self
, reference
, count
) :
451 if not self
._mapping
.has_key(reference
):
452 self
._mapping
[reference
] = PersistentMapping()
453 self
._mapping
[reference
]['pending'] = PersistentMapping()
454 self
._mapping
[reference
]['confirmed'] = 0
456 globalCount
= self
[reference
]
457 delta
= count
- globalCount
458 bid
= self
.getBrowserId()
459 if not self
._mapping
[reference
]['pending'].has_key(bid
) :
460 self
._mapping
[reference
]['pending'][bid
] = delta
462 self
._mapping
[reference
]['pending'][bid
] += delta
465 def __getitem__(self
, reference
) :
466 item
= self
._mapping
[reference
]
467 globalCount
= item
['confirmed']
469 for browserId
, count
in item
['pending'].items() :
470 if self
._checkBrowserId
(browserId
) :
473 del self
._mapping
[reference
]['pending'][browserId
]
477 def get(self
, reference
, default
=0) :
478 if self
._mapping
.has_key(reference
) :
479 return self
[reference
]
483 def getPendingCounter(self
, reference
) :
484 bid
= self
.getBrowserId()
485 if not self
._checkBrowserId
(bid
) :
486 console
.warn('BrowserId not found: %s' % bid
)
489 count
= self
._mapping
[reference
]['pending'].get(bid
, None)
491 console
.warn('No pending data found for browserId %s' % bid
)
496 def confirm(self
, reference
, quantity
) :
497 pending
= self
.getPendingCounter(reference
)
498 if pending
!= quantity
:
499 console
.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending
, quantity
))
501 browserId
= self
.getBrowserId()
502 if self
._mapping
[reference
]['pending'].has_key(browserId
) :
503 del self
._mapping
[reference
]['pending'][browserId
]
504 self
._mapping
[reference
]['confirmed'] += quantity
506 def cancel(self
, reference
, quantity
) :
507 self
._mapping
[reference
]['confirmed'] -= quantity
510 return str(self
._mapping
)