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
48 from paypal
.interface
import PayPalInterface
49 from logging
import getLogger
50 console
= getLogger('Products.photoprint.order')
53 def getPayPalConfig() :
54 zopeConf
= getConfiguration()
56 conf
= zopeConf
.product_config
['photoprint']
58 EnvironmentError("No photoprint configuration found in Zope environment.")
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']}
68 class PrintOrderTemplate(SimpleItem
) :
70 predefined print order
72 implements(IPrintOrderTemplate
)
74 security
= ClassSecurityInfo()
86 self
.description
= description
87 self
.productReference
= productReference
88 self
.maxCopies
= maxCopies
# 0 means unlimited
89 self
.price
= Price(price
, VATRate
)
91 security
.declareProtected(ManagePrintOrderTemplate
, 'edit')
100 self
.description
= description
101 self
.productReference
= productReference
102 self
.maxCopies
= maxCopies
103 self
.price
= Price(price
, VATRate
)
105 security
.declareProtected(ManagePrintOrderTemplate
, 'formWidgetData')
106 def formWidgetData(self
, REQUEST
=None, RESPONSE
=None):
107 """formWidgetData documentation
111 root
= d
.createElement('formdata')
115 return str(getattr(self
, name
, '')).decode('utf-8')
117 id = d
.createElement('id')
118 id.appendChild(d
.createTextNode(self
.getId()))
121 title
= d
.createElement('title')
122 title
.appendChild(d
.createTextNode(gua('title')))
123 root
.appendChild(title
)
125 description
= d
.createElement('description')
126 description
.appendChild(d
.createTextNode(gua('description')))
127 root
.appendChild(description
)
129 productReference
= d
.createElement('productReference')
130 productReference
.appendChild(d
.createTextNode(gua('productReference')))
131 root
.appendChild(productReference
)
133 maxCopies
= d
.createElement('maxCopies')
134 maxCopies
.appendChild(d
.createTextNode(str(self
.maxCopies
)))
135 root
.appendChild(maxCopies
)
137 price
= d
.createElement('price')
138 price
.appendChild(d
.createTextNode(str(self
.price
.taxed
)))
139 root
.appendChild(price
)
141 vatrate
= d
.createElement('VATRate')
142 vatrate
.appendChild(d
.createTextNode(str(self
.price
.vat
)))
143 root
.appendChild(vatrate
)
145 if RESPONSE
is not None :
146 RESPONSE
.setHeader('content-type', 'text/xml; charset=utf-8')
148 manager
= getToolByName(self
, 'caching_policy_manager', None)
149 if manager
is not None:
150 view_name
= 'formWidgetData'
151 headers
= manager
.getHTTPCachingHeaders(
155 for key
, value
in headers
:
157 RESPONSE
.setHeader(key
, value
, literal
=1)
159 RESPONSE
.setHeader(key
, value
)
161 RESPONSE
.setHeader('X-Cache-Headers-Set-By',
162 'CachingPolicyManager: %s' %
163 '/'.join(manager
.getPhysicalPath()))
166 return d
.toxml('utf-8')
169 InitializeClass(PrintOrderTemplate
)
170 PrintOrderTemplateFactory
= Factory(PrintOrderTemplate
)
172 class PrintOrder(PortalContent
, DefaultDublinCoreImpl
) :
174 implements(IPrintOrder
)
175 security
= ClassSecurityInfo()
177 def __init__( self
, id) :
178 DefaultDublinCoreImpl
.__init
__(self
)
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()
190 def amountWithFees(self
) :
191 return self
.price
+ self
.shippingFees
194 security
.declareProtected(ModifyPortalContent
, 'editBilling')
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
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
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')
225 photo
= uidh
.getObject(item
['cmf_uid'])
226 pOptions
= pptool
.getPrintingOptionsContainerFor(photo
)
227 template
= getattr(pOptions
, item
['printing_template'])
229 reference
= template
.productReference
230 quantity
= item
['quantity']
231 uPrice
= template
.price
232 self
.quantity
+= quantity
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
243 self
.price
+= uPrice
* quantity
245 if template
.maxCopies
:
246 counters
= getattr(photo
, COPIES_COUNTERS
)
247 counters
.confirm(reference
, quantity
)
249 self
.items
= tuple(items
)
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
)
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
)
269 self
.shippingFees
= pptool
.getShippingFeesFor(shippable
=self
)
271 cart
._confirmed
= True
272 cart
.pendingOrderPath
= self
.getPhysicalPath()
274 security
.declareProtected(ManagePrintOrders
, 'resetCopiesCounters')
275 def resetCopiesCounters(self
) :
276 pptool
= getToolByName(self
, 'portal_photo_print')
277 uidh
= getToolByName(self
, 'portal_uidhandler')
279 for item
in self
.items
:
280 photo
= uidh
.getObject(item
['cmf_uid'])
281 counters
= getattr(photo
, COPIES_COUNTERS
, None)
283 counters
.cancel(item
['productReference'],
287 def _initPayPalInterface(self
) :
288 config
= getPayPalConfig()
289 config
['API_AUTHENTICATION_MODE'] = '3TOKEN'
290 ppi
= PayPalInterface(**config
)
295 def recordifyPPResp(response
) :
297 d
['zopeTime'] = DateTime()
298 for k
, v
in response
.raw
.iteritems() :
306 security
.declareProtected(ModifyPortalContent
, 'ppSetExpressCheckout')
307 def ppSetExpressCheckout(self
) :
308 utool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
309 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
311 portal
= utool
.getPortalObject()
312 member
= mtool
.getAuthenticatedMember()
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'],
335 if len(self
.items
) > 1 :
336 quantitySum
= reduce(lambda a
, b
: a
['quantity'] + b
['quantity'], self
.items
)
338 quantitySum
= self
.items
[0]['quantity']
339 total
= round(self
.amountWithFees
.getValues()['taxed'], 2)
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"
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
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
)
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
)
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',
375 response
= PrintOrder
.recordifyPPResp(response
)
376 self
._paypalLog
.append(response
)
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.
384 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
385 wfstate
= wtool
.getInfoFor(self
, 'review_state', 'order_workflow')
386 paid
= wfstate
== 'paid'
389 details
= self
.ppGetExpressCheckoutDetails(token
)
391 if payerid
!= details
['PAYERID'] :
394 if details
['ACK'] == 'Success' :
395 response
= self
.ppDoExpressCheckoutPayment(token
,
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
405 , wf_id
='order_workflow'
406 , comments
='Paiement par PayPal')
412 security
.declareProtected(ModifyPortalContent
, 'ppCancel')
413 def ppCancel(self
, token
) :
414 details
= self
.ppGetExpressCheckoutDetails(token
)
416 security
.declareProtected(ManagePortal
, 'getPPLog')
418 return self
._paypalLog
420 def getCustomerSummary(self
) :
422 return {'quantity':self
.quantity
,
426 InitializeClass(PrintOrder
)
427 PrintOrderFactory
= Factory(PrintOrder
)
430 class CopiesCounters(Persistent
, Implicit
) :
433 self
._mapping
= PersistentMapping()
435 def getBrowserId(self
):
436 sdm
= self
.session_data_manager
437 bim
= sdm
.getBrowserIdManager()
438 browserId
= bim
.getBrowserId(create
=1)
441 def _checkBrowserId(self
, browserId
) :
442 sdm
= self
.session_data_manager
443 sd
= sdm
.getSessionDataByKey(browserId
)
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
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
458 self
._mapping
[reference
]['pending'][bid
] += delta
461 def __getitem__(self
, reference
) :
462 item
= self
._mapping
[reference
]
463 globalCount
= item
['confirmed']
465 for browserId
, count
in item
['pending'].items() :
466 if self
._checkBrowserId
(browserId
) :
469 del self
._mapping
[reference
]['pending'][browserId
]
473 def get(self
, reference
, default
=0) :
474 if self
._mapping
.has_key(reference
) :
475 return self
[reference
]
479 def getPendingCounter(self
, reference
) :
480 bid
= self
.getBrowserId()
481 if not self
._checkBrowserId
(bid
) :
482 console
.warn('BrowserId not found: %s' % bid
)
485 count
= self
._mapping
[reference
]['pending'].get(bid
, None)
487 console
.warn('No pending data found for browserId %s' % bid
)
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
))
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
502 def cancel(self
, reference
, quantity
) :
503 self
._mapping
[reference
]['confirmed'] -= quantity
506 return str(self
._mapping
)