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 OFS
.SimpleItem
import SimpleItem
34 from ZTUtils
import make_query
35 from DateTime
import DateTime
36 from Products
.CMFCore
.PortalContent
import PortalContent
37 from Products
.CMFCore
.permissions
import ModifyPortalContent
, View
, ManagePortal
38 from Products
.CMFCore
.utils
import getToolByName
, getUtilityByInterfaceName
39 from Products
.CMFDefault
.DublinCore
import DefaultDublinCoreImpl
40 from Products
.Plinn
.utils
import getPreferredLanguages
41 from interfaces
import IPrintOrderTemplate
, IPrintOrder
42 from permissions
import ManagePrintOrderTemplate
, ManagePrintOrders
43 from price
import Price
44 from xml
.dom
.minidom
import Document
45 from tool
import COPIES_COUNTERS
46 from App
.config
import getConfiguration
47 from paypal
.interface
import PayPalInterface
48 from logging
import getLogger
49 console
= getLogger('Products.photoprint.order')
52 def getPayPalConfig() :
53 zopeConf
= getConfiguration()
55 conf
= zopeConf
.product_config
['photoprint']
57 EnvironmentError("No photoprint configuration found in Zope environment.")
59 ppconf
= {'API_ENVIRONMENT' : conf
['paypal_api_environment'],
60 'API_USERNAME' : conf
['paypal_username'],
61 'API_PASSWORD' : conf
['paypal_password'],
62 'API_SIGNATURE' : conf
['paypal_signature']}
67 class PrintOrderTemplate(SimpleItem
) :
69 predefined print order
71 implements(IPrintOrderTemplate
)
73 security
= ClassSecurityInfo()
85 self
.description
= description
86 self
.productReference
= productReference
87 self
.maxCopies
= maxCopies
# 0 means unlimited
88 self
.price
= Price(price
, VATRate
)
90 security
.declareProtected(ManagePrintOrderTemplate
, 'edit')
99 self
.description
= description
100 self
.productReference
= productReference
101 self
.maxCopies
= maxCopies
102 self
.price
= Price(price
, VATRate
)
104 security
.declareProtected(ManagePrintOrderTemplate
, 'formWidgetData')
105 def formWidgetData(self
, REQUEST
=None, RESPONSE
=None):
106 """formWidgetData documentation
110 root
= d
.createElement('formdata')
114 return str(getattr(self
, name
, '')).decode('utf-8')
116 id = d
.createElement('id')
117 id.appendChild(d
.createTextNode(self
.getId()))
120 title
= d
.createElement('title')
121 title
.appendChild(d
.createTextNode(gua('title')))
122 root
.appendChild(title
)
124 description
= d
.createElement('description')
125 description
.appendChild(d
.createTextNode(gua('description')))
126 root
.appendChild(description
)
128 productReference
= d
.createElement('productReference')
129 productReference
.appendChild(d
.createTextNode(gua('productReference')))
130 root
.appendChild(productReference
)
132 maxCopies
= d
.createElement('maxCopies')
133 maxCopies
.appendChild(d
.createTextNode(str(self
.maxCopies
)))
134 root
.appendChild(maxCopies
)
136 price
= d
.createElement('price')
137 price
.appendChild(d
.createTextNode(str(self
.price
.taxed
)))
138 root
.appendChild(price
)
140 vatrate
= d
.createElement('VATRate')
141 vatrate
.appendChild(d
.createTextNode(str(self
.price
.vat
)))
142 root
.appendChild(vatrate
)
144 if RESPONSE
is not None :
145 RESPONSE
.setHeader('content-type', 'text/xml; charset=utf-8')
147 manager
= getToolByName(self
, 'caching_policy_manager', None)
148 if manager
is not None:
149 view_name
= 'formWidgetData'
150 headers
= manager
.getHTTPCachingHeaders(
154 for key
, value
in headers
:
156 RESPONSE
.setHeader(key
, value
, literal
=1)
158 RESPONSE
.setHeader(key
, value
)
160 RESPONSE
.setHeader('X-Cache-Headers-Set-By',
161 'CachingPolicyManager: %s' %
162 '/'.join(manager
.getPhysicalPath()))
165 return d
.toxml('utf-8')
168 InitializeClass(PrintOrderTemplate
)
169 PrintOrderTemplateFactory
= Factory(PrintOrderTemplate
)
171 class PrintOrder(PortalContent
, DefaultDublinCoreImpl
) :
173 implements(IPrintOrder
)
174 security
= ClassSecurityInfo()
176 def __init__( self
, id) :
177 DefaultDublinCoreImpl
.__init
__(self
)
181 self
.price
= Price(0, 0)
182 # billing and shipping addresses
183 self
.billing
= PersistentMapping()
184 self
.shipping
= PersistentMapping()
185 self
.shippingFees
= Price(0,0)
186 self
._paymentResponse
= PersistentMapping()
189 def amountWithFees(self
) :
190 return self
.price
+ self
.shippingFees
193 security
.declareProtected(ModifyPortalContent
, 'editBilling')
201 self
.billing
['name'] = name
202 self
.billing
['address'] = address
203 self
.billing
['city'] = city
204 self
.billing
['zipcode'] = zipcode
205 self
.billing
['country'] = country
206 self
.billing
['phone'] = phone
208 security
.declareProtected(ModifyPortalContent
, 'editShipping')
209 def editShipping(self
, name
, address
, city
, zipcode
, country
) :
210 self
.shipping
['name'] = name
211 self
.shipping
['address'] = address
212 self
.shipping
['city'] = city
213 self
.shipping
['zipcode'] = zipcode
214 self
.shipping
['country'] = country
216 security
.declarePrivate('loadCart')
217 def loadCart(self
, cart
):
218 pptool
= getToolByName(self
, 'portal_photo_print')
219 uidh
= getToolByName(self
, 'portal_uidhandler')
220 mtool
= getToolByName(self
, 'portal_membership')
224 photo
= uidh
.getObject(item
['cmf_uid'])
225 pOptions
= pptool
.getPrintingOptionsContainerFor(photo
)
226 template
= getattr(pOptions
, item
['printing_template'])
228 reference
= template
.productReference
229 quantity
= item
['quantity']
230 uPrice
= template
.price
231 self
.quantity
+= quantity
233 d
= {'cmf_uid' : item
['cmf_uid']
234 ,'url' : photo
.absolute_url()
235 ,'title' : template
.title
236 ,'description' : template
.description
237 ,'unit_price' : Price(uPrice
._taxed
, uPrice
._rate
)
238 ,'quantity' : quantity
239 ,'productReference' : reference
242 self
.price
+= uPrice
* quantity
244 if template
.maxCopies
:
245 counters
= getattr(photo
, COPIES_COUNTERS
)
246 counters
.confirm(reference
, quantity
)
248 self
.items
= tuple(items
)
250 member
= mtool
.getAuthenticatedMember()
251 mg
= lambda name
: member
.getProperty(name
, '')
252 billing
= {'name' : member
.getMemberFullName(nameBefore
=0)
253 ,'address' : mg('billing_address')
254 ,'city' : mg('billing_city')
255 ,'zipcode' : mg('billing_zipcode')
256 ,'country' : mg('country')
257 ,'phone' : mg('phone') }
258 self
.editBilling(**billing
)
260 sg
= lambda name
: cart
._shippingInfo
.get(name
, '')
261 shipping
= {'name' : sg('shipping_fullname')
262 ,'address' : sg('shipping_address')
263 ,'city' : sg('shipping_city')
264 ,'zipcode' : sg('shipping_zipcode')
265 ,'country' : sg('shipping_country')}
266 self
.editShipping(**shipping
)
268 self
.shippingFees
= pptool
.getShippingFeesFor(shippable
=self
)
270 cart
._confirmed
= True
271 cart
.pendingOrderPath
= self
.getPhysicalPath()
273 security
.declareProtected(ManagePrintOrders
, 'resetCopiesCounters')
274 def resetCopiesCounters(self
) :
275 pptool
= getToolByName(self
, 'portal_photo_print')
276 uidh
= getToolByName(self
, 'portal_uidhandler')
278 for item
in self
.items
:
279 photo
= uidh
.getObject(item
['cmf_uid'])
280 counters
= getattr(photo
, COPIES_COUNTERS
, None)
282 counters
.cancel(item
['productReference'],
286 def _initPayPalInterface(self
) :
287 config
= getPayPalConfig()
288 config
['API_AUTHENTICATION_MODE'] = '3TOKEN'
289 ppi
= PayPalInterface(**config
)
294 def recordifyPPResp(response
) :
296 d
['zopeTime'] = DateTime()
297 for k
, v
in response
.raw
.iteritems() :
305 security
.declareProtected(ModifyPortalContent
, 'ppSetExpressCheckout')
306 def ppSetExpressCheckout(self
) :
307 utool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
308 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
310 portal
= utool
.getPortalObject()
311 member
= mtool
.getAuthenticatedMember()
313 options
= {#'PAYMENTREQUEST_0_AMT' : '99.55', # todo
314 '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(),
319 'ALLOWNOTE' : 0, # The buyer is unable to enter a note to the merchant.
320 'HDRIMG' : '%s/logo.gif' % portal_url
,
321 'EMAIL' : member
.getProperty('email'),
322 'SOLUTIONTYPE' : 'Sole', # Buyer does not need to create a PayPal account to check out. This is referred to as PayPal Account Optional.
323 'LANDINGPAGE' : 'Billing', # Non-PayPal account
324 'BRANDNAME' : portal
.getProperty('title'),
325 'GIFTMESSAGEENABLE' : 0,
326 'GIFTRECEIPTENABLE' : 0,
327 'BUYEREMAILOPTINENABLE' : 0, # Do not enable buyer to provide email address.
328 'NOSHIPPING' : 1, # PayPal does not display shipping address fields whatsoever.
329 # 'PAYMENTREQUEST_0_NOTIFYURL' : TODO
331 'PAYMENTREQUEST_0_SHIPTONAME' : self
.billing
['name'],
332 'PAYMENTREQUEST_0_SHIPTOSTREET' : self
.billing
['address'],
333 'PAYMENTREQUEST_0_SHIPTOCITY' : self
.billing
['city'],
334 'PAYMENTREQUEST_0_SHIPTOZIP' : self
.billing
['zipcode'],
335 'PAYMENTREQUEST_0_SHIPTOPHONENUM' : self
.billing
['phone'],
338 if len(self
.items
) > 1 :
339 quantitySum
= reduce(lambda a
, b
: a
['quantity'] + b
['quantity'], self
.items
)
341 quantitySum
= self
.items
[0]['quantity']
342 total
= round(self
.amountWithFees
.getValues()['taxed'], 2)
344 options
['L_PAYMENTREQUEST_0_NAME0'] = 'Commande realis photo ref. %s' % self
.getId()
345 if quantitySum
== 1 :
346 options
['L_PAYMENTREQUEST_0_DESC0'] = "Commande d'un tirage photographique"
348 options
['L_PAYMENTREQUEST_0_DESC0'] = 'Commande de %d tirages photographiques' % quantitySum
349 options
['L_PAYMENTREQUEST_0_AMT0'] = total
350 options
['PAYMENTINFO_0_SHIPPINGAMT'] = round(self
.shippingFees
.getValues()['taxed'], 2)
351 # options['L_PAYMENTREQUEST_0_TAXAMT0'] = tax
352 # options['L_PAYMENTREQUEST_0_QTY%d' % n] = 1
353 options
['PAYMENTREQUEST_0_AMT'] = total
355 ppi
= self
._initPayPalInterface
()
356 response
= ppi
.set_express_checkout(**options
)
357 response
= PrintOrder
.recordifyPPResp(response
)
358 # self._paypalLog.append(response)
359 response
['url'] = ppi
.generate_express_checkout_redirect_url(response
['TOKEN'])
360 console
.info(options
)
361 console
.info(response
)
364 security
.declarePrivate('ppGetExpressCheckoutDetails')
365 def ppGetExpressCheckoutDetails(self
, token
) :
366 ppi
= self
._initPayPalInterface
()
367 response
= ppi
.get_express_checkout_details(TOKEN
=token
)
368 response
= PrintOrder
.recordifyPPResp(response
)
369 # self._paypalLog.append(response)
372 security
.declarePrivate('ppDoExpressCheckoutPayment')
373 def ppDoExpressCheckoutPayment(self
, token
, payerid
, amt
) :
374 ppi
= self
._initPayPalInterface
()
375 response
= ppi
.do_express_checkout_payment(PAYMENTREQUEST_0_PAYMENTACTION
='Sale',
376 PAYMENTREQUEST_0_AMT
=amt
,
377 PAYMENTREQUEST_0_CURRENCYCODE
='EUR',
380 response
= PrintOrder
.recordifyPPResp(response
)
381 # self._paypalLog.append(response)
384 security
.declareProtected(ModifyPortalContent
, 'ppPay')
385 def ppPay(self
, token
, payerid
):
386 # assure le paiement paypal en une passe :
387 # récupération des détails et validation de la transaction.
389 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
390 wfstate
= wtool
.getInfoFor(self
, 'review_state', 'order_workflow')
391 paid
= wfstate
== 'paid'
394 details
= self
.ppGetExpressCheckoutDetails(token
)
396 if payerid
!= details
['PAYERID'] :
399 if details
['ACK'] == 'Success' :
400 response
= self
.ppDoExpressCheckoutPayment(token
,
403 if response
['ACK'] == 'Success' and \
404 response
['PAYMENTINFO_0_ACK'] == 'Success' and \
405 response
['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Completed' :
406 self
.paid
= (DateTime(), 'paypal')
407 wtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
408 wtool
.doActionFor( self
410 , wf_id
='order_workflow'
411 , comments
='Paiement par PayPal')
417 security
.declareProtected(ModifyPortalContent
, 'ppCancel')
418 def ppCancel(self
, token
) :
419 details
= self
.ppGetExpressCheckoutDetails(token
)
421 security
.declareProtected(ManagePortal
, 'getPPLog')
423 return self
._paypalLog
425 def getCustomerSummary(self
) :
427 return {'quantity':self
.quantity
,
431 InitializeClass(PrintOrder
)
432 PrintOrderFactory
= Factory(PrintOrder
)
435 class CopiesCounters(Persistent
, Implicit
) :
438 self
._mapping
= PersistentMapping()
440 def getBrowserId(self
):
441 sdm
= self
.session_data_manager
442 bim
= sdm
.getBrowserIdManager()
443 browserId
= bim
.getBrowserId(create
=1)
446 def _checkBrowserId(self
, browserId
) :
447 sdm
= self
.session_data_manager
448 sd
= sdm
.getSessionDataByKey(browserId
)
451 def __setitem__(self
, reference
, count
) :
452 if not self
._mapping
.has_key(reference
):
453 self
._mapping
[reference
] = PersistentMapping()
454 self
._mapping
[reference
]['pending'] = PersistentMapping()
455 self
._mapping
[reference
]['confirmed'] = 0
457 globalCount
= self
[reference
]
458 delta
= count
- globalCount
459 bid
= self
.getBrowserId()
460 if not self
._mapping
[reference
]['pending'].has_key(bid
) :
461 self
._mapping
[reference
]['pending'][bid
] = delta
463 self
._mapping
[reference
]['pending'][bid
] += delta
466 def __getitem__(self
, reference
) :
467 item
= self
._mapping
[reference
]
468 globalCount
= item
['confirmed']
470 for browserId
, count
in item
['pending'].items() :
471 if self
._checkBrowserId
(browserId
) :
474 del self
._mapping
[reference
]['pending'][browserId
]
478 def get(self
, reference
, default
=0) :
479 if self
._mapping
.has_key(reference
) :
480 return self
[reference
]
484 def getPendingCounter(self
, reference
) :
485 bid
= self
.getBrowserId()
486 if not self
._checkBrowserId
(bid
) :
487 console
.warn('BrowserId not found: %s' % bid
)
490 count
= self
._mapping
[reference
]['pending'].get(bid
, None)
492 console
.warn('No pending data found for browserId %s' % bid
)
497 def confirm(self
, reference
, quantity
) :
498 pending
= self
.getPendingCounter(reference
)
499 if pending
!= quantity
:
500 console
.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending
, quantity
))
502 browserId
= self
.getBrowserId()
503 if self
._mapping
[reference
]['pending'].has_key(browserId
) :
504 del self
._mapping
[reference
]['pending'][browserId
]
505 self
._mapping
[reference
]['confirmed'] += quantity
507 def cancel(self
, reference
, quantity
) :
508 self
._mapping
[reference
]['confirmed'] -= quantity
511 return str(self
._mapping
)