Paiement opérationnel avec PayPal. Il reste à loguer les échanges PayPal / Zope.
[photoprint.git] / order.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2009-2013 Benoît PIN <benoit.pin@ensmp.fr> #
5 # #
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. #
10 # #
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. #
15 # #
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 #######################################################################################
20 """
21 Print order classes
22
23
24
25 """
26
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 utils import Message as _
45 from utils import translate
46 from xml.dom.minidom import Document
47 from tool import COPIES_COUNTERS
48 from App.config import getConfiguration
49 from paypal.interface import PayPalInterface
50 from logging import getLogger
51 console = getLogger('Products.photoprint.order')
52
53
54 def getPayPalConfig() :
55 zopeConf = getConfiguration()
56 try :
57 conf = zopeConf.product_config['photoprint']
58 except KeyError :
59 EnvironmentError("No photoprint configuration found in Zope environment.")
60
61 ppconf = {'API_ENVIRONMENT' : conf['paypal_api_environment'],
62 'API_USERNAME' : conf['paypal_username'],
63 'API_PASSWORD' : conf['paypal_password'],
64 'API_SIGNATURE' : conf['paypal_signature']}
65
66 return ppconf
67
68
69 class PrintOrderTemplate(SimpleItem) :
70 """
71 predefined print order
72 """
73 implements(IPrintOrderTemplate)
74
75 security = ClassSecurityInfo()
76
77 def __init__(self
78 , id
79 , title=''
80 , description=''
81 , productReference=''
82 , maxCopies=0
83 , price=0
84 , VATRate=0) :
85 self.id = id
86 self.title = title
87 self.description = description
88 self.productReference = productReference
89 self.maxCopies = maxCopies # 0 means unlimited
90 self.price = Price(price, VATRate)
91
92 security.declareProtected(ManagePrintOrderTemplate, 'edit')
93 def edit( self
94 , title=''
95 , description=''
96 , productReference=''
97 , maxCopies=0
98 , price=0
99 , VATRate=0 ) :
100 self.title = title
101 self.description = description
102 self.productReference = productReference
103 self.maxCopies = maxCopies
104 self.price = Price(price, VATRate)
105
106 security.declareProtected(ManagePrintOrderTemplate, 'formWidgetData')
107 def formWidgetData(self, REQUEST=None, RESPONSE=None):
108 """formWidgetData documentation
109 """
110 d = Document()
111 d.encoding = 'utf-8'
112 root = d.createElement('formdata')
113 d.appendChild(root)
114
115 def gua(name) :
116 return str(getattr(self, name, '')).decode('utf-8')
117
118 id = d.createElement('id')
119 id.appendChild(d.createTextNode(self.getId()))
120 root.appendChild(id)
121
122 title = d.createElement('title')
123 title.appendChild(d.createTextNode(gua('title')))
124 root.appendChild(title)
125
126 description = d.createElement('description')
127 description.appendChild(d.createTextNode(gua('description')))
128 root.appendChild(description)
129
130 productReference = d.createElement('productReference')
131 productReference.appendChild(d.createTextNode(gua('productReference')))
132 root.appendChild(productReference)
133
134 maxCopies = d.createElement('maxCopies')
135 maxCopies.appendChild(d.createTextNode(str(self.maxCopies)))
136 root.appendChild(maxCopies)
137
138 price = d.createElement('price')
139 price.appendChild(d.createTextNode(str(self.price.taxed)))
140 root.appendChild(price)
141
142 vatrate = d.createElement('VATRate')
143 vatrate.appendChild(d.createTextNode(str(self.price.vat)))
144 root.appendChild(vatrate)
145
146 if RESPONSE is not None :
147 RESPONSE.setHeader('content-type', 'text/xml; charset=utf-8')
148
149 manager = getToolByName(self, 'caching_policy_manager', None)
150 if manager is not None:
151 view_name = 'formWidgetData'
152 headers = manager.getHTTPCachingHeaders(
153 self, view_name, {}
154 )
155
156 for key, value in headers:
157 if key == 'ETag':
158 RESPONSE.setHeader(key, value, literal=1)
159 else:
160 RESPONSE.setHeader(key, value)
161 if headers:
162 RESPONSE.setHeader('X-Cache-Headers-Set-By',
163 'CachingPolicyManager: %s' %
164 '/'.join(manager.getPhysicalPath()))
165
166
167 return d.toxml('utf-8')
168
169
170 InitializeClass(PrintOrderTemplate)
171 PrintOrderTemplateFactory = Factory(PrintOrderTemplate)
172
173 class PrintOrder(PortalContent, DefaultDublinCoreImpl) :
174
175 implements(IPrintOrder)
176 security = ClassSecurityInfo()
177
178 def __init__( self, id) :
179 DefaultDublinCoreImpl.__init__(self)
180 self.id = id
181 self.items = []
182 self.quantity = 0
183 self.price = Price(0, 0)
184 # billing and shipping addresses
185 self.billing = PersistentMapping()
186 self.shipping = PersistentMapping()
187 self.shippingFees = Price(0,0)
188 self._paymentResponse = PersistentMapping()
189
190 @property
191 def amountWithFees(self) :
192 return self.price + self.shippingFees
193
194
195 security.declareProtected(ModifyPortalContent, 'editBilling')
196 def editBilling(self
197 , name
198 , address
199 , city
200 , zipcode
201 , country
202 , phone) :
203 self.billing['name'] = name
204 self.billing['address'] = address
205 self.billing['city'] = city
206 self.billing['zipcode'] = zipcode
207 self.billing['country'] = country
208 self.billing['phone'] = phone
209
210 security.declareProtected(ModifyPortalContent, 'editShipping')
211 def editShipping(self, name, address, city, zipcode, country) :
212 self.shipping['name'] = name
213 self.shipping['address'] = address
214 self.shipping['city'] = city
215 self.shipping['zipcode'] = zipcode
216 self.shipping['country'] = country
217
218 security.declarePrivate('loadCart')
219 def loadCart(self, cart):
220 pptool = getToolByName(self, 'portal_photo_print')
221 uidh = getToolByName(self, 'portal_uidhandler')
222 mtool = getToolByName(self, 'portal_membership')
223
224 items = []
225 for item in cart :
226 photo = uidh.getObject(item['cmf_uid'])
227 pOptions = pptool.getPrintingOptionsContainerFor(photo)
228 template = getattr(pOptions, item['printing_template'])
229
230 reference = template.productReference
231 quantity = item['quantity']
232 uPrice = template.price
233 self.quantity += quantity
234
235 d = {'cmf_uid' : item['cmf_uid']
236 ,'url' : photo.absolute_url()
237 ,'title' : template.title
238 ,'description' : template.description
239 ,'unit_price' : Price(uPrice._taxed, uPrice._rate)
240 ,'quantity' : quantity
241 ,'productReference' : reference
242 }
243 items.append(d)
244 self.price += uPrice * quantity
245 # confirm counters
246 if template.maxCopies :
247 counters = getattr(photo, COPIES_COUNTERS)
248 counters.confirm(reference, quantity)
249
250 self.items = tuple(items)
251
252 member = mtool.getAuthenticatedMember()
253 mg = lambda name : member.getProperty(name, '')
254 billing = {'name' : member.getMemberFullName(nameBefore=0)
255 ,'address' : mg('billing_address')
256 ,'city' : mg('billing_city')
257 ,'zipcode' : mg('billing_zipcode')
258 ,'country' : mg('country')
259 ,'phone' : mg('phone') }
260 self.editBilling(**billing)
261
262 sg = lambda name : cart._shippingInfo.get(name, '')
263 shipping = {'name' : sg('shipping_fullname')
264 ,'address' : sg('shipping_address')
265 ,'city' : sg('shipping_city')
266 ,'zipcode' : sg('shipping_zipcode')
267 ,'country' : sg('shipping_country')}
268 self.editShipping(**shipping)
269
270 self.shippingFees = pptool.getShippingFeesFor(shippable=self)
271
272 cart._confirmed = True
273 cart.pendingOrderPath = self.getPhysicalPath()
274
275 security.declareProtected(ManagePrintOrders, 'resetCopiesCounters')
276 def resetCopiesCounters(self) :
277 pptool = getToolByName(self, 'portal_photo_print')
278 uidh = getToolByName(self, 'portal_uidhandler')
279
280 for item in self.items :
281 photo = uidh.getObject(item['cmf_uid'])
282 counters = getattr(photo, COPIES_COUNTERS, None)
283 if counters :
284 counters.cancel(item['productReference'],
285 item['quantity'])
286
287
288 def _initPayPalInterface(self) :
289 config = getPayPalConfig()
290 config['API_AUTHENTICATION_MODE'] = '3TOKEN'
291 ppi = PayPalInterface(**config)
292 return ppi
293
294
295 @staticmethod
296 def recordifyPPResp(response) :
297 d = {}
298 d['zopeTime'] = DateTime()
299 for k, v in response.raw.iteritems() :
300 if len(v) == 1 :
301 d[k] = v[0]
302 else :
303 d[k] = v
304 return d
305
306 # paypal api
307 security.declareProtected(ModifyPortalContent, 'ppSetExpressCheckout')
308 def ppSetExpressCheckout(self) :
309 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
310 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
311 portal_url = utool()
312 portal = utool.getPortalObject()
313 member = mtool.getAuthenticatedMember()
314
315 options = {#'PAYMENTREQUEST_0_AMT' : '99.55', # todo
316 'PAYMENTREQUEST_0_CURRENCYCODE' : 'EUR',
317 'PAYMENTREQUEST_0_PAYMENTACTION' : 'Sale',
318 'RETURNURL' : '%s/photoprint_order_confirm' % self.absolute_url(),
319 'CANCELURL' : '%s/photoprint_order_cancel' % self.absolute_url(),
320 # 'CALLBACK' : TODO
321 'ALLOWNOTE' : 0, # The buyer is unable to enter a note to the merchant.
322 'HDRIMG' : '%s/logo.gif' % portal_url,
323 'EMAIL' : member.getProperty('email'),
324 'SOLUTIONTYPE' : 'Sole', # Buyer does not need to create a PayPal account to check out. This is referred to as PayPal Account Optional.
325 'LANDINGPAGE' : 'Billing', # Non-PayPal account
326 'BRANDNAME' : portal.getProperty('title'),
327 'GIFTMESSAGEENABLE' : 0,
328 'GIFTRECEIPTENABLE' : 0,
329 'BUYEREMAILOPTINENABLE' : 0, # Do not enable buyer to provide email address.
330 'NOSHIPPING' : 1, # PayPal does not display shipping address fields whatsoever.
331 # 'PAYMENTREQUEST_0_NOTIFYURL' : TODO
332
333 'PAYMENTREQUEST_0_SHIPTONAME' : self.billing['name'],
334 'PAYMENTREQUEST_0_SHIPTOSTREET' : self.billing['address'],
335 'PAYMENTREQUEST_0_SHIPTOCITY' : self.billing['city'],
336 'PAYMENTREQUEST_0_SHIPTOZIP' : self.billing['zipcode'],
337 'PAYMENTREQUEST_0_SHIPTOPHONENUM' : self.billing['phone'],
338 }
339
340 quantitySum = reduce(lambda a, b : a['quantity'] + b['quantity'], self.items)
341 priceSum = reduce(lambda a, b : a['unit_price'] * a['quantity'] + b['unit_price'] * b['quantity'], self.items)
342 priceValues = priceSum.getValues()
343 total = round(self.amountWithFees.getValues()['taxed'], 2)
344
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"
348 else :
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
355
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)
363 return response
364
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)
371 return response
372
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',
379 TOKEN=token,
380 PAYERID=payerid)
381 response = PrintOrder.recordifyPPResp(response)
382 # self._paypalLog.append(response)
383 return response
384
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.
389
390 wtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool')
391 wfstate = wtool.getInfoFor(self, 'review_state', 'order_workflow')
392 paid = wfstate == 'paid'
393
394 if not paid :
395 details = self.ppGetExpressCheckoutDetails(token)
396
397 if payerid != details['PAYERID'] :
398 return False
399
400 if details['ACK'] == 'Success' :
401 response = self.ppDoExpressCheckoutPayment(token,
402 payerid,
403 details['AMT'])
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
410 , 'paypal_pay'
411 , wf_id='order_workflow'
412 , comments='Paiement par PayPal')
413 return True
414 return False
415 else :
416 return True
417
418 security.declareProtected(ModifyPortalContent, 'ppCancel')
419 def ppCancel(self, token) :
420 details = self.ppGetExpressCheckoutDetails(token)
421
422 security.declareProtected(ManagePortal, 'getPPLog')
423 def getPPLog(self) :
424 return self._paypalLog
425
426
427
428 # security.declareProtected(View, 'getPaymentRequest')
429 # def getPaymentRequest(self) :
430 # config = _getCyberplusConfig()
431 # requester = CyberplusRequester(config)
432 # hereurl = self.absolute_url()
433 # amount = self.price + self.shippingFees
434 # amount = amount.getValues()['taxed']
435 # amount = amount * 100
436 # amount = str(int(round(amount, 0)))
437 # pptool = getToolByName(self, 'portal_photo_print')
438 # transaction_id = pptool.getNextTransactionId()
439 #
440 # userLanguages = getPreferredLanguages(self)
441 # for pref in userLanguages :
442 # lang = pref.split('-')[0]
443 # if lang in CYBERPLUS_LANGUAGES :
444 # break
445 # else :
446 # lang = 'en'
447 #
448 # options = { 'amount': amount
449 # ,'cancel_return_url' : '%s/paymentCancelHandler' % hereurl
450 # ,'normal_return_url' : '%s/paymentManualResponseHandler' % hereurl
451 # ,'automatic_response_url' :'%s/paymentAutoResponseHandler' % hereurl
452 # ,'transaction_id' : transaction_id
453 # ,'order_id' : self.getId()
454 # ,'language' : lang
455 # }
456 # req = requester.generateRequest(options)
457 # return req
458 #
459 # def _decodeCyberplusResponse(self, form) :
460 # config = _getCyberplusConfig()
461 # responder = CyberplusResponder(config)
462 # response = responder.getResponse(form)
463 # return response
464 #
465 # def _compareWithAutoResponse(self, manu) :
466 # keys = manu.keys()
467 # auto = self._paymentResponse
468 # autoKeys = auto.keys()
469 # if len(keys) != len(autoKeys) :
470 # console.warn('Manual has not the same keys.\nauto: %r\nmanual: %r' % \
471 # (sorted(autoKeys), sorted(keys)))
472 # else :
473 # for k, v in manu.items() :
474 # if not auto.has_key(k) :
475 # console.warn('%r field only found in manual response.' % k)
476 # else :
477 # if v != auto[k] :
478 # console.warn('data mismatch for %r\nauto: %r\nmanual: %r' % (k, auto[k], v))
479 #
480 # def _checkOrderId(self, response) :
481 # expected = self.getId()
482 # assert expected == response['order_id'], \
483 # "Cyberplus response transaction_id doesn't match the order object:\n" \
484 # "expected: %s\n" \
485 # "found: %s" % (expected, response['transaction_id'])
486
487 # def _executeOrderWfTransition(self, response) :
488 # if CyberplusResponder.transactionAccepted(response) :
489 # wfaction = 'auto_accept_payment'
490 # elif CyberplusResponder.transactionRefused(response) :
491 # self.resetCopiesCounters()
492 # wfaction = 'auto_refuse_payment'
493 # elif CyberplusResponder.transactionCanceled(response) :
494 # wfaction = 'auto_cancel_order'
495 # else :
496 # # transaction failed
497 # wfaction = 'auto_transaction_failed'
498 #
499 # wtool = getToolByName(self, 'portal_workflow')
500 # wf = wtool.getWorkflowById('order_workflow')
501 # tdef = wf.transitions.get(wfaction)
502 # wf._changeStateOf(self, tdef)
503 # wtool._reindexWorkflowVariables(self)
504
505 # security.declarePublic('paymentAutoResponseHandler')
506 # @postonly
507 # def paymentAutoResponseHandler(self, REQUEST) :
508 # """\
509 # Handle cyberplus payment auto response.
510 # """
511 # response = self._decodeCyberplusResponse(REQUEST.form)
512 # self._checkOrderId(response)
513 # self._paymentResponse.update(response)
514 # self._executeOrderWfTransition(response)
515 #
516 # @postonly
517 # def paymentManualResponseHandler(self, REQUEST) :
518 # """\
519 # Handle cyberplus payment manual response.
520 # """
521 # response = self._decodeCyberplusResponse(REQUEST.form)
522 # self._checkOrderId(response)
523 #
524 # autoResponse = self._paymentResponse
525 # if not autoResponse :
526 # console.warn('Manual response handled before auto response at %s' % '/'.join(self.getPhysicalPath()))
527 # self._paymentResponse.update(response)
528 # self._executeOrderWfTransition(response)
529 # else :
530 # self._compareWithAutoResponse(response)
531 #
532 # url = '%s?%s' % (self.absolute_url(),
533 # make_query(portal_status_message=translate('Your payment is complete.', self).encode('utf-8'))
534 # )
535 # return REQUEST.RESPONSE.redirect(url)
536 #
537 # @postonly
538 # def paymentCancelHandler(self, REQUEST) :
539 # """\
540 # Handle cyberplus cancel response.
541 # This handler can be invoqued in two cases:
542 # - the user cancel the payment form
543 # - the payment transaction has been refused
544 # """
545 # response = self._decodeCyberplusResponse(REQUEST.form)
546 # self._checkOrderId(response)
547 #
548 # if self._paymentResponse :
549 # # normaly, it happens when the transaction is refused by cyberplus.
550 # self._compareWithAutoResponse(response)
551 #
552 #
553 # if CyberplusResponder.transactionRefused(response) :
554 # if not self._paymentResponse :
555 # console.warn('Manual response handled before auto response at %s' % '/'.join(self.getPhysicalPath()))
556 # self._paymentResponse.update(response)
557 # self._executeOrderWfTransition(response)
558 #
559 # msg = 'Your payment has been refused.'
560 #
561 # else :
562 # self._executeOrderWfTransition(response)
563 # msg = 'Your payment has been canceled. You will be able to pay later.'
564 #
565 # url = '%s?%s' % (self.absolute_url(),
566 # make_query(portal_status_message= \
567 # translate(msg, self).encode('utf-8'))
568 # )
569 # return REQUEST.RESPONSE.redirect(url)
570
571
572 def getCustomerSummary(self) :
573 ' '
574 return {'quantity':self.quantity,
575 'price':self.price}
576
577
578 InitializeClass(PrintOrder)
579 PrintOrderFactory = Factory(PrintOrder)
580
581
582 class CopiesCounters(Persistent, Implicit) :
583
584 def __init__(self):
585 self._mapping = PersistentMapping()
586
587 def getBrowserId(self):
588 sdm = self.session_data_manager
589 bim = sdm.getBrowserIdManager()
590 browserId = bim.getBrowserId(create=1)
591 return browserId
592
593 def _checkBrowserId(self, browserId) :
594 sdm = self.session_data_manager
595 sd = sdm.getSessionDataByKey(browserId)
596 return not not sd
597
598 def __setitem__(self, reference, count) :
599 if not self._mapping.has_key(reference):
600 self._mapping[reference] = PersistentMapping()
601 self._mapping[reference]['pending'] = PersistentMapping()
602 self._mapping[reference]['confirmed'] = 0
603
604 globalCount = self[reference]
605 delta = count - globalCount
606 bid = self.getBrowserId()
607 if not self._mapping[reference]['pending'].has_key(bid) :
608 self._mapping[reference]['pending'][bid] = delta
609 else :
610 self._mapping[reference]['pending'][bid] += delta
611
612
613 def __getitem__(self, reference) :
614 item = self._mapping[reference]
615 globalCount = item['confirmed']
616
617 for browserId, count in item['pending'].items() :
618 if self._checkBrowserId(browserId) :
619 globalCount += count
620 else :
621 del self._mapping[reference]['pending'][browserId]
622
623 return globalCount
624
625 def get(self, reference, default=0) :
626 if self._mapping.has_key(reference) :
627 return self[reference]
628 else :
629 return default
630
631 def getPendingCounter(self, reference) :
632 bid = self.getBrowserId()
633 if not self._checkBrowserId(bid) :
634 console.warn('BrowserId not found: %s' % bid)
635 return 0
636
637 count = self._mapping[reference]['pending'].get(bid, None)
638 if count is None :
639 console.warn('No pending data found for browserId %s' % bid)
640 return 0
641 else :
642 return count
643
644 def confirm(self, reference, quantity) :
645 pending = self.getPendingCounter(reference)
646 if pending != quantity :
647 console.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending, quantity))
648
649 browserId = self.getBrowserId()
650 if self._mapping[reference]['pending'].has_key(browserId) :
651 del self._mapping[reference]['pending'][browserId]
652 self._mapping[reference]['confirmed'] += quantity
653
654 def cancel(self, reference, quantity) :
655 self._mapping[reference]['confirmed'] -= quantity
656
657 def __str__(self):
658 return str(self._mapping)