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
.discount
= 0 # discount ratio in percent
187 self
.price
= Price(0, 0)
188 # billing and shipping addresses
189 self
.billing
= PersistentMapping()
190 self
.shipping
= PersistentMapping()
191 self
.shippingFees
= Price(0,0)
192 self
._paypalLog
= PersistentList()
195 def amountWithFees(self
) :
196 coeff
= (100 - self
.discount
) / 100.
197 return self
.price
* coeff
+ self
.shippingFees
200 security
.declareProtected(ModifyPortalContent
, 'editBilling')
208 self
.billing
['name'] = name
209 self
.billing
['address'] = address
210 self
.billing
['city'] = city
211 self
.billing
['zipcode'] = zipcode
212 self
.billing
['country'] = country
213 self
.billing
['phone'] = phone
215 security
.declareProtected(ModifyPortalContent
, 'editShipping')
216 def editShipping(self
, name
, address
, city
, zipcode
, country
) :
217 self
.shipping
['name'] = name
218 self
.shipping
['address'] = address
219 self
.shipping
['city'] = city
220 self
.shipping
['zipcode'] = zipcode
221 self
.shipping
['country'] = country
223 security
.declarePrivate('loadCart')
224 def loadCart(self
, cart
):
225 pptool
= getToolByName(self
, 'portal_photo_print')
226 uidh
= getToolByName(self
, 'portal_uidhandler')
227 mtool
= getToolByName(self
, 'portal_membership')
228 utool
= getToolByName(self
, 'portal_url')
232 photo
= uidh
.getObject(item
['cmf_uid'])
233 pOptions
= pptool
.getPrintingOptionsContainerFor(photo
)
234 template
= getattr(pOptions
, item
['printing_template'])
236 reference
= template
.productReference
237 quantity
= item
['quantity']
238 uPrice
= template
.price
239 self
.quantity
+= quantity
241 d
= {'cmf_uid' : item
['cmf_uid']
242 ,'url' : photo
.absolute_url()
243 ,'title' : template
.title
244 ,'description' : template
.description
245 ,'unit_price' : Price(uPrice
._taxed
, uPrice
._rate
)
246 ,'quantity' : quantity
247 ,'productReference' : reference
250 self
.price
+= uPrice
* quantity
252 if template
.maxCopies
:
253 counters
= getattr(photo
, COPIES_COUNTERS
)
254 counters
.confirm(reference
, quantity
)
256 self
.items
= tuple(items
)
257 discount_script
= getattr(utool
.getPortalObject(), 'photoprint_discount', None)
259 self
.discount
= discount_script(self
.price
, self
.quantity
)
261 member
= mtool
.getAuthenticatedMember()
262 mg
= lambda name
: member
.getProperty(name
, '')
263 billing
= {'name' : member
.getMemberFullName(nameBefore
=0)
264 ,'address' : mg('billing_address')
265 ,'city' : mg('billing_city')
266 ,'zipcode' : mg('billing_zipcode')
267 ,'country' : mg('country')
268 ,'phone' : mg('phone') }
269 self
.editBilling(**billing
)
271 sg
= lambda name
: cart
._shippingInfo
.get(name
, '')
272 shipping
= {'name' : sg('shipping_fullname')
273 ,'address' : sg('shipping_address')
274 ,'city' : sg('shipping_city')
275 ,'zipcode' : sg('shipping_zipcode')
276 ,'country' : sg('shipping_country')}
277 self
.editShipping(**shipping
)
279 self
.shippingFees
= pptool
.getShippingFeesFor(shippable
=self
)
281 cart
._confirmed
= True
282 cart
.pendingOrderPath
= self
.getPhysicalPath()
284 security
.declareProtected(ManagePrintOrders
, 'resetCopiesCounters')
285 def resetCopiesCounters(self
) :
286 pptool
= getToolByName(self
, 'portal_photo_print')
287 uidh
= getToolByName(self
, 'portal_uidhandler')
289 for item
in self
.items
:
290 photo
= uidh
.getObject(item
['cmf_uid'])
291 counters
= getattr(photo
, COPIES_COUNTERS
, None)
293 counters
.cancel(item
['productReference'],
297 def _initPayPalInterface(self
) :
298 config
= getPayPalConfig()
299 config
['API_AUTHENTICATION_MODE'] = '3TOKEN'
300 ppi
= PayPalInterface(**config
)
305 def recordifyPPResp(response
) :
307 d
['zopeTime'] = DateTime()
308 for k
, v
in response
.raw
.iteritems() :
316 security
.declareProtected(ModifyPortalContent
, 'ppSetExpressCheckout')
317 def ppSetExpressCheckout(self
) :
318 utool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
319 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
321 portal
= utool
.getPortalObject()
322 member
= mtool
.getAuthenticatedMember()
324 options
= {'PAYMENTREQUEST_0_CURRENCYCODE' : 'EUR',
325 'PAYMENTREQUEST_0_PAYMENTACTION' : 'Sale',
326 'RETURNURL' : '%s/photoprint_order_confirm' % self
.absolute_url(),
327 'CANCELURL' : '%s/photoprint_order_cancel' % self
.absolute_url(),
328 'ALLOWNOTE' : 0, # The buyer is unable to enter a note to the merchant.
329 'HDRIMG' : '%s/logo.gif' % portal_url
,
330 'EMAIL' : member
.getProperty('email'),
331 'SOLUTIONTYPE' : 'Sole', # Buyer does not need to create a PayPal account to check out. This is referred to as PayPal Account Optional.
332 'LANDINGPAGE' : 'Billing', # Non-PayPal account
333 'BRANDNAME' : portal
.getProperty('title'),
334 'GIFTMESSAGEENABLE' : 0,
335 'GIFTRECEIPTENABLE' : 0,
336 'BUYEREMAILOPTINENABLE' : 0, # Do not enable buyer to provide email address.
337 'NOSHIPPING' : 1, # PayPal does not display shipping address fields whatsoever.
338 'PAYMENTREQUEST_0_SHIPTONAME' : self
.billing
['name'],
339 'PAYMENTREQUEST_0_SHIPTOSTREET' : self
.billing
['address'],
340 'PAYMENTREQUEST_0_SHIPTOCITY' : self
.billing
['city'],
341 'PAYMENTREQUEST_0_SHIPTOZIP' : self
.billing
['zipcode'],
342 'PAYMENTREQUEST_0_SHIPTOPHONENUM' : self
.billing
['phone'],
345 if len(self
.items
) > 1 :
346 quantitySum
= reduce(lambda a
, b
: a
+ b
, [item
['quantity'] for item
in self
.items
])
348 quantitySum
= self
.items
[0]['quantity']
349 total
= round(self
.amountWithFees
.getValues()['taxed'], 2)
351 options
['L_PAYMENTREQUEST_0_NAME0'] = 'Commande photo ref. %s' % self
.getId()
352 if quantitySum
== 1 :
353 options
['L_PAYMENTREQUEST_0_DESC0'] = "Commande d'un tirage photographique"
355 options
['L_PAYMENTREQUEST_0_DESC0'] = 'Commande de %d tirages photographiques' % quantitySum
356 options
['L_PAYMENTREQUEST_0_AMT0'] = total
357 options
['PAYMENTINFO_0_SHIPPINGAMT'] = round(self
.shippingFees
.getValues()['taxed'], 2)
358 options
['PAYMENTREQUEST_0_AMT'] = total
360 ppi
= self
._initPayPalInterface
()
361 response
= ppi
.set_express_checkout(**options
)
362 response
= PrintOrder
.recordifyPPResp(response
)
363 self
._paypalLog
.append(response
)
364 response
['url'] = ppi
.generate_express_checkout_redirect_url(response
['TOKEN'])
365 console
.info(options
)
366 console
.info(response
)
369 security
.declarePrivate('ppGetExpressCheckoutDetails')
370 def ppGetExpressCheckoutDetails(self
, token
) :
371 ppi
= self
._initPayPalInterface
()
372 response
= ppi
.get_express_checkout_details(TOKEN
=token
)
373 response
= PrintOrder
.recordifyPPResp(response
)
374 self
._paypalLog
.append(response
)
377 security
.declarePrivate('ppDoExpressCheckoutPayment')
378 def ppDoExpressCheckoutPayment(self
, token
, payerid
, amt
) :
379 ppi
= self
._initPayPalInterface
()
380 response
= ppi
.do_express_checkout_payment(PAYMENTREQUEST_0_PAYMENTACTION
='Sale',
381 PAYMENTREQUEST_0_AMT
=amt
,
382 PAYMENTREQUEST_0_CURRENCYCODE
='EUR',
385 response
= PrintOrder
.recordifyPPResp(response
)
386 self
._paypalLog
.append(response
)
389 security
.declareProtected(ModifyPortalContent
, 'ppPay')
390 def ppPay(self
, token
, payerid
):
391 # assure le paiement paypal en une passe :
392 # récupération des détails et validation de la transaction.
394 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
395 wfstate
= wtool
.getInfoFor(self
, 'review_state', 'order_workflow')
396 paid
= wfstate
== 'paid'
399 details
= self
.ppGetExpressCheckoutDetails(token
)
401 if payerid
!= details
['PAYERID'] :
404 if details
['ACK'] == 'Success' :
405 response
= self
.ppDoExpressCheckoutPayment(token
,
408 if response
['ACK'] == 'Success' and \
409 response
['PAYMENTINFO_0_ACK'] == 'Success' and \
410 response
['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Completed' :
411 self
.paid
= (DateTime(), 'paypal')
412 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
413 wtool
.doActionFor( self
415 , wf_id
='order_workflow'
416 , comments
='Paiement par PayPal')
422 security
.declareProtected(ModifyPortalContent
, 'ppCancel')
423 def ppCancel(self
, token
) :
424 details
= self
.ppGetExpressCheckoutDetails(token
)
426 security
.declareProtected(ManagePortal
, 'getPPLog')
428 return self
._paypalLog
430 def getCustomerSummary(self
) :
432 return {'quantity':self
.quantity
,
436 InitializeClass(PrintOrder
)
437 PrintOrderFactory
= Factory(PrintOrder
)
440 class CopiesCounters(Persistent
, Implicit
) :
443 self
._mapping
= PersistentMapping()
445 def getBrowserId(self
):
446 sdm
= self
.session_data_manager
447 bim
= sdm
.getBrowserIdManager()
448 browserId
= bim
.getBrowserId(create
=1)
451 def _checkBrowserId(self
, browserId
) :
452 sdm
= self
.session_data_manager
453 sd
= sdm
.getSessionDataByKey(browserId
)
456 def __setitem__(self
, reference
, count
) :
457 if not self
._mapping
.has_key(reference
):
458 self
._mapping
[reference
] = PersistentMapping()
459 self
._mapping
[reference
]['pending'] = PersistentMapping()
460 self
._mapping
[reference
]['confirmed'] = 0
462 globalCount
= self
[reference
]
463 delta
= count
- globalCount
464 bid
= self
.getBrowserId()
465 if not self
._mapping
[reference
]['pending'].has_key(bid
) :
466 self
._mapping
[reference
]['pending'][bid
] = delta
468 self
._mapping
[reference
]['pending'][bid
] += delta
471 def __getitem__(self
, reference
) :
472 item
= self
._mapping
[reference
]
473 globalCount
= item
['confirmed']
475 for browserId
, count
in item
['pending'].items() :
476 if self
._checkBrowserId
(browserId
) :
479 del self
._mapping
[reference
]['pending'][browserId
]
483 def get(self
, reference
, default
=0) :
484 if self
._mapping
.has_key(reference
) :
485 return self
[reference
]
489 def getPendingCounter(self
, reference
) :
490 bid
= self
.getBrowserId()
491 if not self
._checkBrowserId
(bid
) :
492 console
.warn('BrowserId not found: %s' % bid
)
495 count
= self
._mapping
[reference
]['pending'].get(bid
, None)
497 console
.warn('No pending data found for browserId %s' % bid
)
502 def confirm(self
, reference
, quantity
) :
503 pending
= self
.getPendingCounter(reference
)
504 if pending
!= quantity
:
505 console
.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending
, quantity
))
507 browserId
= self
.getBrowserId()
508 if self
._mapping
[reference
]['pending'].has_key(browserId
) :
509 del self
._mapping
[reference
]['pending'][browserId
]
510 self
._mapping
[reference
]['confirmed'] += quantity
512 def cancel(self
, reference
, quantity
) :
513 self
._mapping
[reference
]['confirmed'] -= quantity
516 return str(self
._mapping
)