X-Git-Url: https://scm.cri.minesparis.psl.eu/git/photoprint.git/blobdiff_plain/ccaba0f54cbd41250107d36a49154353425da8b3..b521f0db73266a7bd6b69e6a9891046a03c482ee:/Products/photoprint/order.py diff --git a/Products/photoprint/order.py b/Products/photoprint/order.py new file mode 100755 index 0000000..6966ff3 --- /dev/null +++ b/Products/photoprint/order.py @@ -0,0 +1,516 @@ +# -*- coding: utf-8 -*- +####################################################################################### +# Plinn - http://plinn.org # +# Copyright (C) 2009-2013 Benoît PIN # +# # +# This program is free software; you can redistribute it and/or # +# modify it under the terms of the GNU General Public License # +# as published by the Free Software Foundation; either version 2 # +# of the License, or (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program; if not, write to the Free Software # +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # +####################################################################################### +""" +Print order classes + + + +""" + +from Globals import InitializeClass, PersistentMapping, Persistent +from Acquisition import Implicit +from AccessControl import ClassSecurityInfo +from AccessControl.requestmethod import postonly +from zope.interface import implements +from zope.component.factory import Factory +from persistent.list import PersistentList +from OFS.SimpleItem import SimpleItem +from ZTUtils import make_query +from DateTime import DateTime +from Products.CMFCore.PortalContent import PortalContent +from Products.CMFCore.permissions import ModifyPortalContent, View, ManagePortal +from Products.CMFCore.utils import getToolByName, getUtilityByInterfaceName +from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl +from Products.Plinn.utils import getPreferredLanguages +from interfaces import IPrintOrderTemplate, IPrintOrder +from permissions import ManagePrintOrderTemplate, ManagePrintOrders +from price import Price +from xml.dom.minidom import Document +from tool import COPIES_COUNTERS +from App.config import getConfiguration +try : + from paypal.interface import PayPalInterface + paypalAvailable = True +except ImportError : + paypalAvailable = False +from logging import getLogger +console = getLogger('Products.photoprint.order') + + +def getPayPalConfig() : + zopeConf = getConfiguration() + try : + conf = zopeConf.product_config['photoprint'] + except KeyError : + EnvironmentError("No photoprint configuration found in Zope environment.") + + ppconf = {'API_ENVIRONMENT' : conf['paypal_api_environment'], + 'API_USERNAME' : conf['paypal_username'], + 'API_PASSWORD' : conf['paypal_password'], + 'API_SIGNATURE' : conf['paypal_signature']} + + return ppconf + + +class PrintOrderTemplate(SimpleItem) : + """ + predefined print order + """ + implements(IPrintOrderTemplate) + + security = ClassSecurityInfo() + + def __init__(self + , id + , title='' + , description='' + , productReference='' + , maxCopies=0 + , price=0 + , VATRate=0) : + self.id = id + self.title = title + self.description = description + self.productReference = productReference + self.maxCopies = maxCopies # 0 means unlimited + self.price = Price(price, VATRate) + + security.declareProtected(ManagePrintOrderTemplate, 'edit') + def edit( self + , title='' + , description='' + , productReference='' + , maxCopies=0 + , price=0 + , VATRate=0 ) : + self.title = title + self.description = description + self.productReference = productReference + self.maxCopies = maxCopies + self.price = Price(price, VATRate) + + security.declareProtected(ManagePrintOrderTemplate, 'formWidgetData') + def formWidgetData(self, REQUEST=None, RESPONSE=None): + """formWidgetData documentation + """ + d = Document() + d.encoding = 'utf-8' + root = d.createElement('formdata') + d.appendChild(root) + + def gua(name) : + return str(getattr(self, name, '')).decode('utf-8') + + id = d.createElement('id') + id.appendChild(d.createTextNode(self.getId())) + root.appendChild(id) + + title = d.createElement('title') + title.appendChild(d.createTextNode(gua('title'))) + root.appendChild(title) + + description = d.createElement('description') + description.appendChild(d.createTextNode(gua('description'))) + root.appendChild(description) + + productReference = d.createElement('productReference') + productReference.appendChild(d.createTextNode(gua('productReference'))) + root.appendChild(productReference) + + maxCopies = d.createElement('maxCopies') + maxCopies.appendChild(d.createTextNode(str(self.maxCopies))) + root.appendChild(maxCopies) + + price = d.createElement('price') + price.appendChild(d.createTextNode(str(self.price.taxed))) + root.appendChild(price) + + vatrate = d.createElement('VATRate') + vatrate.appendChild(d.createTextNode(str(self.price.vat))) + root.appendChild(vatrate) + + if RESPONSE is not None : + RESPONSE.setHeader('content-type', 'text/xml; charset=utf-8') + + manager = getToolByName(self, 'caching_policy_manager', None) + if manager is not None: + view_name = 'formWidgetData' + headers = manager.getHTTPCachingHeaders( + self, view_name, {} + ) + + for key, value in headers: + if key == 'ETag': + RESPONSE.setHeader(key, value, literal=1) + else: + RESPONSE.setHeader(key, value) + if headers: + RESPONSE.setHeader('X-Cache-Headers-Set-By', + 'CachingPolicyManager: %s' % + '/'.join(manager.getPhysicalPath())) + + + return d.toxml('utf-8') + + +InitializeClass(PrintOrderTemplate) +PrintOrderTemplateFactory = Factory(PrintOrderTemplate) + +class PrintOrder(PortalContent, DefaultDublinCoreImpl) : + + implements(IPrintOrder) + security = ClassSecurityInfo() + + def __init__( self, id) : + DefaultDublinCoreImpl.__init__(self) + self.id = id + self.items = [] + self.quantity = 0 + self.discount = 0 # discount ratio in percent + self.price = Price(0, 0) + # billing and shipping addresses + self.billing = PersistentMapping() + self.shipping = PersistentMapping() + self.shippingFees = Price(0,0) + self._paypalLog = PersistentList() + + @property + def amountWithFees(self) : + coeff = (100 - self.discount) / 100. + return self.price * coeff + self.shippingFees + + + security.declareProtected(ModifyPortalContent, 'editBilling') + def editBilling(self + , name + , address + , city + , zipcode + , country + , phone) : + self.billing['name'] = name + self.billing['address'] = address + self.billing['city'] = city + self.billing['zipcode'] = zipcode + self.billing['country'] = country + self.billing['phone'] = phone + + security.declareProtected(ModifyPortalContent, 'editShipping') + def editShipping(self, name, address, city, zipcode, country) : + self.shipping['name'] = name + self.shipping['address'] = address + self.shipping['city'] = city + self.shipping['zipcode'] = zipcode + self.shipping['country'] = country + + security.declarePrivate('loadCart') + def loadCart(self, cart): + pptool = getToolByName(self, 'portal_photo_print') + uidh = getToolByName(self, 'portal_uidhandler') + mtool = getToolByName(self, 'portal_membership') + utool = getToolByName(self, 'portal_url') + + items = [] + for item in cart : + photo = uidh.getObject(item['cmf_uid']) + pOptions = pptool.getPrintingOptionsContainerFor(photo) + template = getattr(pOptions, item['printing_template']) + + reference = template.productReference + quantity = item['quantity'] + uPrice = template.price + self.quantity += quantity + + d = {'cmf_uid' : item['cmf_uid'] + ,'url' : photo.absolute_url() + ,'title' : template.title + ,'description' : template.description + ,'unit_price' : Price(uPrice._taxed, uPrice._rate) + ,'quantity' : quantity + ,'productReference' : reference + } + items.append(d) + self.price += uPrice * quantity + # confirm counters + if template.maxCopies : + counters = getattr(photo, COPIES_COUNTERS) + counters.confirm(reference, quantity) + + self.items = tuple(items) + discount_script = getattr(utool.getPortalObject(), 'photoprint_discount', None) + if discount_script : + self.discount = discount_script(self.price, self.quantity) + + member = mtool.getAuthenticatedMember() + mg = lambda name : member.getProperty(name, '') + billing = {'name' : member.getMemberFullName(nameBefore=0) + ,'address' : mg('billing_address') + ,'city' : mg('billing_city') + ,'zipcode' : mg('billing_zipcode') + ,'country' : mg('country') + ,'phone' : mg('phone') } + self.editBilling(**billing) + + sg = lambda name : cart._shippingInfo.get(name, '') + shipping = {'name' : sg('shipping_fullname') + ,'address' : sg('shipping_address') + ,'city' : sg('shipping_city') + ,'zipcode' : sg('shipping_zipcode') + ,'country' : sg('shipping_country')} + self.editShipping(**shipping) + + self.shippingFees = pptool.getShippingFeesFor(shippable=self) + + cart._confirmed = True + cart.pendingOrderPath = self.getPhysicalPath() + + security.declareProtected(ManagePrintOrders, 'resetCopiesCounters') + def resetCopiesCounters(self) : + pptool = getToolByName(self, 'portal_photo_print') + uidh = getToolByName(self, 'portal_uidhandler') + + for item in self.items : + photo = uidh.getObject(item['cmf_uid']) + counters = getattr(photo, COPIES_COUNTERS, None) + if counters : + counters.cancel(item['productReference'], + item['quantity']) + + + def _initPayPalInterface(self) : + config = getPayPalConfig() + config['API_AUTHENTICATION_MODE'] = '3TOKEN' + ppi = PayPalInterface(**config) + return ppi + + + @staticmethod + def recordifyPPResp(response) : + d = {} + d['zopeTime'] = DateTime() + for k, v in response.raw.iteritems() : + if len(v) == 1 : + d[k] = v[0] + else : + d[k] = v + return d + + # paypal api + security.declareProtected(ModifyPortalContent, 'ppSetExpressCheckout') + def ppSetExpressCheckout(self) : + utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool') + mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool') + portal_url = utool() + portal = utool.getPortalObject() + member = mtool.getAuthenticatedMember() + + options = {'PAYMENTREQUEST_0_CURRENCYCODE' : 'EUR', + 'PAYMENTREQUEST_0_PAYMENTACTION' : 'Sale', + 'RETURNURL' : '%s/photoprint_order_confirm' % self.absolute_url(), + 'CANCELURL' : '%s/photoprint_order_cancel' % self.absolute_url(), + 'ALLOWNOTE' : 0, # The buyer is unable to enter a note to the merchant. + 'HDRIMG' : '%s/logo.gif' % portal_url, + 'EMAIL' : member.getProperty('email'), + 'SOLUTIONTYPE' : 'Sole', # Buyer does not need to create a PayPal account to check out. This is referred to as PayPal Account Optional. + 'LANDINGPAGE' : 'Billing', # Non-PayPal account + 'BRANDNAME' : portal.getProperty('title'), + 'GIFTMESSAGEENABLE' : 0, + 'GIFTRECEIPTENABLE' : 0, + 'BUYEREMAILOPTINENABLE' : 0, # Do not enable buyer to provide email address. + 'NOSHIPPING' : 1, # PayPal does not display shipping address fields whatsoever. + 'PAYMENTREQUEST_0_SHIPTONAME' : self.billing['name'], + 'PAYMENTREQUEST_0_SHIPTOSTREET' : self.billing['address'], + 'PAYMENTREQUEST_0_SHIPTOCITY' : self.billing['city'], + 'PAYMENTREQUEST_0_SHIPTOZIP' : self.billing['zipcode'], + 'PAYMENTREQUEST_0_SHIPTOPHONENUM' : self.billing['phone'], + } + + if len(self.items) > 1 : + quantitySum = reduce(lambda a, b : a + b, [item['quantity'] for item in self.items]) + else : + quantitySum = self.items[0]['quantity'] + total = round(self.amountWithFees.getValues()['taxed'], 2) + + options['L_PAYMENTREQUEST_0_NAME0'] = 'Commande photo ref. %s' % self.getId() + if quantitySum == 1 : + options['L_PAYMENTREQUEST_0_DESC0'] = "Commande d'un tirage photographique" + else : + options['L_PAYMENTREQUEST_0_DESC0'] = 'Commande de %d tirages photographiques' % quantitySum + options['L_PAYMENTREQUEST_0_AMT0'] = total + options['PAYMENTINFO_0_SHIPPINGAMT'] = round(self.shippingFees.getValues()['taxed'], 2) + options['PAYMENTREQUEST_0_AMT'] = total + + ppi = self._initPayPalInterface() + response = ppi.set_express_checkout(**options) + response = PrintOrder.recordifyPPResp(response) + self._paypalLog.append(response) + response['url'] = ppi.generate_express_checkout_redirect_url(response['TOKEN']) + console.info(options) + console.info(response) + return response + + security.declarePrivate('ppGetExpressCheckoutDetails') + def ppGetExpressCheckoutDetails(self, token) : + ppi = self._initPayPalInterface() + response = ppi.get_express_checkout_details(TOKEN=token) + response = PrintOrder.recordifyPPResp(response) + self._paypalLog.append(response) + return response + + security.declarePrivate('ppDoExpressCheckoutPayment') + def ppDoExpressCheckoutPayment(self, token, payerid, amt) : + ppi = self._initPayPalInterface() + response = ppi.do_express_checkout_payment(PAYMENTREQUEST_0_PAYMENTACTION='Sale', + PAYMENTREQUEST_0_AMT=amt, + PAYMENTREQUEST_0_CURRENCYCODE='EUR', + TOKEN=token, + PAYERID=payerid) + response = PrintOrder.recordifyPPResp(response) + self._paypalLog.append(response) + return response + + security.declareProtected(ModifyPortalContent, 'ppPay') + def ppPay(self, token, payerid): + # assure le paiement paypal en une passe : + # récupération des détails et validation de la transaction. + + wtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool') + wfstate = wtool.getInfoFor(self, 'review_state', 'order_workflow') + paid = wfstate == 'paid' + + if not paid : + details = self.ppGetExpressCheckoutDetails(token) + + if payerid != details['PAYERID'] : + return False + + if details['ACK'] == 'Success' : + response = self.ppDoExpressCheckoutPayment(token, + payerid, + details['AMT']) + if response['ACK'] == 'Success' and \ + response['PAYMENTINFO_0_ACK'] == 'Success' and \ + response['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Completed' : + self.paid = (DateTime(), 'paypal') + wtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IWorkflowTool') + wtool.doActionFor( self + , 'paypal_pay' + , wf_id='order_workflow' + , comments='Paiement par PayPal') + return True + return False + else : + return True + + security.declareProtected(ModifyPortalContent, 'ppCancel') + def ppCancel(self, token) : + details = self.ppGetExpressCheckoutDetails(token) + + security.declareProtected(ManagePortal, 'getPPLog') + def getPPLog(self) : + return self._paypalLog + + def getCustomerSummary(self) : + ' ' + return {'quantity':self.quantity, + 'price':self.price} + + +InitializeClass(PrintOrder) +PrintOrderFactory = Factory(PrintOrder) + + +class CopiesCounters(Persistent, Implicit) : + + def __init__(self): + self._mapping = PersistentMapping() + + def getBrowserId(self): + sdm = self.session_data_manager + bim = sdm.getBrowserIdManager() + browserId = bim.getBrowserId(create=1) + return browserId + + def _checkBrowserId(self, browserId) : + sdm = self.session_data_manager + sd = sdm.getSessionDataByKey(browserId) + return not not sd + + def __setitem__(self, reference, count) : + if not self._mapping.has_key(reference): + self._mapping[reference] = PersistentMapping() + self._mapping[reference]['pending'] = PersistentMapping() + self._mapping[reference]['confirmed'] = 0 + + globalCount = self[reference] + delta = count - globalCount + bid = self.getBrowserId() + if not self._mapping[reference]['pending'].has_key(bid) : + self._mapping[reference]['pending'][bid] = delta + else : + self._mapping[reference]['pending'][bid] += delta + + + def __getitem__(self, reference) : + item = self._mapping[reference] + globalCount = item['confirmed'] + + for browserId, count in item['pending'].items() : + if self._checkBrowserId(browserId) : + globalCount += count + else : + del self._mapping[reference]['pending'][browserId] + + return globalCount + + def get(self, reference, default=0) : + if self._mapping.has_key(reference) : + return self[reference] + else : + return default + + def getPendingCounter(self, reference) : + bid = self.getBrowserId() + if not self._checkBrowserId(bid) : + console.warn('BrowserId not found: %s' % bid) + return 0 + + count = self._mapping[reference]['pending'].get(bid, None) + if count is None : + console.warn('No pending data found for browserId %s' % bid) + return 0 + else : + return count + + def confirm(self, reference, quantity) : + pending = self.getPendingCounter(reference) + if pending != quantity : + console.warn('Pending quantity mismatch with the confirmed value: (%d, %d)' % (pending, quantity)) + + browserId = self.getBrowserId() + if self._mapping[reference]['pending'].has_key(browserId) : + del self._mapping[reference]['pending'][browserId] + self._mapping[reference]['confirmed'] += quantity + + def cancel(self, reference, quantity) : + self._mapping[reference]['confirmed'] -= quantity + + def __str__(self): + return str(self._mapping)