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_AMT' : '99.55', # todo
315 'PAYMENTREQUEST_0_CURRENCYCODE' : 'EUR',
316 'PAYMENTREQUEST_0_PAYMENTACTION' : 'Sale',
317 'RETURNURL' : '%s/photoprint_order_confirm' % self
.absolute_url(),
318 'CANCELURL' : '%s/photoprint_order_cancel' % self
.absolute_url(),
320 'ALLOWNOTE' : 0, # The buyer is unable to enter a note to the merchant.
321 'HDRIMG' : '%s/logo.gif' % portal_url
,
322 'EMAIL' : member
.getProperty('email'),
323 'SOLUTIONTYPE' : 'Sole', # Buyer does not need to create a PayPal account to check out. This is referred to as PayPal Account Optional.
324 'LANDINGPAGE' : 'Billing', # Non-PayPal account
325 'BRANDNAME' : portal
.getProperty('title'),
326 'GIFTMESSAGEENABLE' : 0,
327 'GIFTRECEIPTENABLE' : 0,
328 'BUYEREMAILOPTINENABLE' : 0, # Do not enable buyer to provide email address.
329 'NOSHIPPING' : 1, # PayPal does not display shipping address fields whatsoever.
330 # 'PAYMENTREQUEST_0_NOTIFYURL' : TODO
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['L_PAYMENTREQUEST_0_TAXAMT0'] = tax
353 # options['L_PAYMENTREQUEST_0_QTY%d' % n] = 1
354 options
['PAYMENTREQUEST_0_AMT'] = total
356 ppi
= self
._initPayPalInterface
()
357 response
= ppi
.set_express_checkout(**options
)
358 response
= PrintOrder
.recordifyPPResp(response
)
359 self
._paypalLog
.append(response
)
360 response
['url'] = ppi
.generate_express_checkout_redirect_url(response
['TOKEN'])
361 console
.info(options
)
362 console
.info(response
)
365 security
.declarePrivate('ppGetExpressCheckoutDetails')
366 def ppGetExpressCheckoutDetails(self
, token
) :
367 ppi
= self
._initPayPalInterface
()
368 response
= ppi
.get_express_checkout_details(TOKEN
=token
)
369 response
= PrintOrder
.recordifyPPResp(response
)
370 self
._paypalLog
.append(response
)
373 security
.declarePrivate('ppDoExpressCheckoutPayment')
374 def ppDoExpressCheckoutPayment(self
, token
, payerid
, amt
) :
375 ppi
= self
._initPayPalInterface
()
376 response
= ppi
.do_express_checkout_payment(PAYMENTREQUEST_0_PAYMENTACTION
='Sale',
377 PAYMENTREQUEST_0_AMT
=amt
,
378 PAYMENTREQUEST_0_CURRENCYCODE
='EUR',
381 response
= PrintOrder
.recordifyPPResp(response
)
382 self
._paypalLog
.append(response
)
385 security
.declareProtected(ModifyPortalContent
, 'ppPay')
386 def ppPay(self
, token
, payerid
):
387 # assure le paiement paypal en une passe :
388 # récupération des détails et validation de la transaction.
390 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
391 wfstate
= wtool
.getInfoFor(self
, 'review_state', 'order_workflow')
392 paid
= wfstate
== 'paid'
395 details
= self
.ppGetExpressCheckoutDetails(token
)
397 if payerid
!= details
['PAYERID'] :
400 if details
['ACK'] == 'Success' :
401 response
= self
.ppDoExpressCheckoutPayment(token
,
404 if response
['ACK'] == 'Success' and \
405 response
['PAYMENTINFO_0_ACK'] == 'Success' and \
406 response
['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Completed' :
407 self
.paid
= (DateTime(), 'paypal')
408 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
409 wtool
.doActionFor( self
411 , wf_id
='order_workflow'
412 , comments
='Paiement par PayPal')
418 security
.declareProtected(ModifyPortalContent
, 'ppCancel')
419 def ppCancel(self
, token
) :
420 details
= self
.ppGetExpressCheckoutDetails(token
)
422 security
.declareProtected(ManagePortal
, 'getPPLog')
424 return self
._paypalLog
426 def getCustomerSummary(self
) :
428 return {'quantity':self
.quantity
,
432 InitializeClass(PrintOrder
)
433 PrintOrderFactory
= Factory(PrintOrder
)
436 class CopiesCounters(Persistent
, Implicit
) :
439 self
._mapping
= PersistentMapping()
441 def getBrowserId(self
):
442 sdm
= self
.session_data_manager
443 bim
= sdm
.getBrowserIdManager()
444 browserId
= bim
.getBrowserId(create
=1)
447 def _checkBrowserId(self
, browserId
) :
448 sdm
= self
.session_data_manager
449 sd
= sdm
.getSessionDataByKey(browserId
)
452 def __setitem__(self
, reference
, count
) :
453 if not self
._mapping
.has_key(reference
):
454 self
._mapping
[reference
] = PersistentMapping()
455 self
._mapping
[reference
]['pending'] = PersistentMapping()
456 self
._mapping
[reference
]['confirmed'] = 0
458 globalCount
= self
[reference
]
459 delta
= count
- globalCount
460 bid
= self
.getBrowserId()
461 if not self
._mapping
[reference
]['pending'].has_key(bid
) :
462 self
._mapping
[reference
]['pending'][bid
] = delta
464 self
._mapping
[reference
]['pending'][bid
] += delta
467 def __getitem__(self
, reference
) :
468 item
= self
._mapping
[reference
]
469 globalCount
= item
['confirmed']
471 for browserId
, count
in item
['pending'].items() :
472 if self
._checkBrowserId
(browserId
) :
475 del self
._mapping
[reference
]['pending'][browserId
]
479 def get(self
, reference
, default
=0) :
480 if self
._mapping
.has_key(reference
) :
481 return self
[reference
]
485 def getPendingCounter(self
, reference
) :
486 bid
= self
.getBrowserId()
487 if not self
._checkBrowserId
(bid
) :
488 console
.warn('BrowserId not found: %s' % bid
)
491 count
= self
._mapping
[reference
]['pending'].get(bid
, None)
493 console
.warn('No pending data found for browserId %s' % bid
)
498 def confirm(self
, reference
, quantity
) :
499 pending
= self
.getPendingCounter(reference
)
500 if pending
!= quantity
:
501 console
.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending
, quantity
))
503 browserId
= self
.getBrowserId()
504 if self
._mapping
[reference
]['pending'].has_key(browserId
) :
505 del self
._mapping
[reference
]['pending'][browserId
]
506 self
._mapping
[reference
]['confirmed'] += quantity
508 def cancel(self
, reference
, quantity
) :
509 self
._mapping
[reference
]['confirmed'] -= quantity
512 return str(self
._mapping
)