"""
A form action adapter that e-mails input.
"""
__author__ = 'Rob LaRubbio <rob@onenw.org>'
__docformat__ = 'plaintext'
########################
# The formMailerAdapter code and schema borrow heavily
# from PloneFormGen <http://plone.org/products/ploneformgen>
# by Steve McMahon <steve@dcn.org> which borrows heavily
# from PloneFormMailer <http://plone.org/products/ploneformmailer>
# by Jens Klein and Reinout van Rees.
#
# Author: Jens Klein <jens.klein@jensquadrat.com>
#
# Copyright: (c) 2004 by jens quadrat, Klein & Partner KEG, Austria
# Licence: GNU General Public Licence (GPL) Version 2 or later
#######################
import logging
from cPickle import loads, dumps
from AccessControl import ClassSecurityInfo
from Acquisition import aq_parent
from Products.Archetypes.public import *
from Products.Archetypes.utils import OrderedDict
from Products.Archetypes.utils import shasattr
from Products.ATContentTypes.content.base import registerATCT
from Products.CMFCore.permissions import View, ModifyPortalContent
from Products.PloneFormGen import dollarReplace
from Products.PloneFormGen.content.fields import *
from Products.PloneFormGen.content.ya_gpg import gpg
from Products.PloneFormGen.content.formMailerAdapter import FormMailerAdapter, formMailerAdapterSchema
from getpaid.formgen.config import PROJECTNAME
from email.Header import Header
# Get Paid events
import zope
from getpaid.core.interfaces import workflow_states, IShoppingCartUtility, IShippableOrder, IShippingRateService, IShippableLineItem
from zope.annotation.interfaces import IAnnotations
from Acquisition import aq_base
logger = logging.getLogger("GetPaidFormMailer")
getPaidFormMailerAdapterSchema = formMailerAdapterSchema.copy()
class GetPaidFormMailerAdapter(FormMailerAdapter):
""" A form action adapter that will e-mail form input. """
schema = getPaidFormMailerAdapterSchema
portal_type = meta_type = 'GetPaidFormMailerAdapter'
archetype_name = 'GetPaid Mailer Adapter'
content_icon = 'mailaction.gif'
security = ClassSecurityInfo()
security.declarePrivate('onSuccess')
def onSuccess(self, fields, REQUEST=None):
"""
e-mails data.
"""
all_fields = [f for f in fields
if not (f.isLabel() or f.isFileField()) and not (getattr(self, 'showAll', True) and f.getServerSide())]
# which form fields should we show?
if getattr(self, 'showAll', True):
live_fields = all_fields
else:
live_fields = \
[f for f in all_fields
if f.fgField.getName() in getattr(self, 'showFields', ())]
if not getattr(self, 'includeEmpties', True):
all_fields = live_fields
live_fields = []
for f in all_fields:
value = f.htmlValue(request)
if value and value != 'No Input':
live_fields.append(f)
formFields = []
for field in live_fields:
formFields.append( (field.title, field.htmlValue(REQUEST)) )
scu = zope.component.getUtility(IShoppingCartUtility)
# I need to figure out which cart to get. The default, or multishot
# to do that I find the first getpaid adapter (if there are multiple
# then this will fail). I then look for the attr success_callback
formFolder = aq_parent(self)
adapter = formFolder.objectValues('GetpaidPFGAdapter')[0]
cart = None
if (adapter.success_callback == '_one_page_checkout_success'):
cartKey = "multishot:%s" % formFolder.title
cart = scu.get(self, key=cartKey)
else:
cart = scu.get(self, create=True)
if (cart == None):
logger.info("Unable to get cart")
else:
annotation = IAnnotations(cart)
if "getpaid.formgen.mailer.adapters" in annotation:
adapters = annotation["getpaid.formgen.mailer.adapters"]
if not self.title in adapters:
adapters.append(self.title)
else:
adapters = [self.title]
annotation["getpaid.formgen.mailer.adapters"] = adapters
annotationKey = "getpaid.formgen.mailer.%s" % self.title
data = {}
data['formFields'] = formFields
attachments = self.get_form_attachments(fields, REQUEST)
data['attachments'] = attachments
# This is a complete hack and I can't believe it isn't
# going to come back and break in the future
# I just need some of the request for my supers
# implementation. I can't pickle an aq wrapped object, so
# I resort to this. Perhaps it's better to reimplement
# the code from my super I'm reusing?
# (note I did end up pulling more code into this class)
req = {}
req['form'] = {}
# ignore any form element that is a file upload field
for key in REQUEST.form.keys():
value = REQUEST.form[key]
if value and not isinstance(value, FileUpload):
req['form'][key] = value
for key in getattr(self, 'xinfo_headers', []):
if REQUEST.has_key(key):
req[key] = REQUEST[key]
data['request'] = req
# adapter is attached to the main ZODB
# If using mount points then storage of adapter fails
# due to cross db commit. We pickle to get a
# clean copy to store.
data['adapter'] = loads( dumps( aq_base(self) ) )
annotation[annotationKey] = data
security.declarePrivate('_dreplace')
def _dreplace(self, s):
_form = getattr(self.REQUEST, 'form', None)
if _form is None:
_form = self.REQUEST['form']
return dollarReplace.DollarVarReplacer(_form).sub(s)
security.declarePrivate('getMailBodyDefault')
def getMailBodyDefault(self):
""" Get default mail body from our tool """
return DEFAULT_MAILTEMPLATE_BODY
# Todo implement this to pull attachments out of the annotation
def get_attachments(self, fields, request):
"""Return all attachments that were uploaded in form
and stored in an annotation
"""
scu = zope.component.getUtility(IShoppingCartUtility)
cart = scu.get(self, create=True)
attachments = []
if (cart == None):
logger.info("Unable to get cart")
else:
annotation = IAnnotations(cart)
annotationKey = "getpaid.formgen.mailer.%s" % self.title
if annotationKey in annotation:
data = annotation[annotationKey]
attachments = data["attachments"]
return attachments
def get_form_attachments(self, fields, request):
"""Return all attachments uploaded in form.
"""
from ZPublisher.HTTPRequest import FileUpload
attachments = []
for field in fields:
if field.isFileField():
file = request.form.get('%s_file' % field.__name__, None)
if file and isinstance(file, FileUpload) and file.filename != '':
file.seek(0) # rewind
data = file.read()
filename = file.filename
mimetype, enc = guess_content_type(filename, data, None)
attachments.append((filename, mimetype, enc, data))
return attachments
security.declarePrivate('get_mail_body')
def get_mail_body(self, fields, **kwargs):
"""Returns the mail-body with footer.
"""
bodyfield = self.getField('body_pt')
# pass both the bare_fields (fgFields only) and full fields.
# bare_fields for compatability with older templates,
# full fields to enable access to htmlValue
body = bodyfield.get(self, formFields=fields, **kwargs)
if isinstance(body, unicode):
body = body.encode(self.getCharset())
keyid = getattr(self, 'gpg_keyid', None)
encryption = gpg and keyid
if encryption:
bodygpg = gpg.encrypt(body, keyid)
if bodygpg.strip():
body = bodygpg
return body
# I override this method since I don't have a real request object.
# I have a dict instead.
security.declarePrivate('get_header_body_tuple')
def get_header_body_tuple(self, fields, request,
from_addr=None, to_addr=None,
subject=None, **kwargs):
"""Return header and body of e-mail as an 3-tuple:
(header, additional_header, body)
header is a dictionary, additional header is a list, body is a StringIO
Keyword arguments:
request -- (optional) alternate request object to use
"""
body = self.get_mail_body(fields, **kwargs)
# fields = self.fgFields()
# get Reply-To
reply_addr = None
if shasattr(self, 'replyto_field'):
reply_addr = request['form'].get(self.replyto_field, None)
# get subject header
nosubject = '(no subject)'
if shasattr(self, 'subjectOverride') and self.getRawSubjectOverride():
# subject has a TALES override
subject = self.getSubjectOverride().strip()
else:
subject = getattr(self, 'msg_subject', nosubject)
subjectField = request['form'].get(self.subject_field, None)
if subjectField is not None:
subject = subjectField
else:
# we only do subject expansion if there's no field chosen
subject = dollarReplace.DollarVarReplacer(request['form']).sub(subject)
# Get From address
if shasattr(self, 'senderOverride') and self.getRawSenderOverride():
from_addr = self.getSenderOverride().strip()
else:
pprops = getToolByName(self, 'portal_properties')
site_props = getToolByName(pprops, 'site_properties')
portal = getToolByName(self, 'portal_url').getPortalObject()
from_addr = from_addr or site_props.getProperty('email_from_address') or \
portal.getProperty('email_from_address')
# Get To address and full name
if shasattr(self, 'recipientOverride') and self.getRawRecipientOverride():
recip_email = self.getRecipientOverride()
else:
recip_email = None
if shasattr(self, 'to_field'):
recip_email = request['form'].get(self.to_field, None)
if not recip_email:
recip_email = self.recipient_email
recip_email = self._destFormat( recip_email )
recip_name = self.recipient_name.encode('utf-8')
# if no to_addr and no recip_email specified, use owner adress if possible.
# if not, fall back to portal email_from_address.
# if still no destination, raise an assertion exception.
if not recip_email and not to_addr:
ownerinfo = self.getOwner()
ownerid=ownerinfo.getId()
pms = getToolByName(self, 'portal_membership')
userdest = pms.getMemberById(ownerid)
toemail = userdest.getProperty('email', '')
if not toemail:
portal = getToolByName(self, 'portal_url').getPortalObject()
toemail = portal.getProperty('email_from_address')
assert toemail, """
Unable to mail form input because no recipient address has been specified.
Please check the recipient settings of the PloneFormGen "Mailer" within the
current form folder.
"""
to = '%s <%s>' %(ownerid,toemail)
else:
to = to_addr or '%s %s' % (recip_name, recip_email)
headerinfo = OrderedDict()
headerinfo['To'] = self.secure_header_line(to)
headerinfo['From'] = self.secure_header_line(from_addr)
if reply_addr:
headerinfo['Reply-To'] = self.secure_header_line(reply_addr)
# transform subject into mail header encoded string
portal = getToolByName(self, 'portal_url').getPortalObject()
email_charset = portal.getProperty('email_charset', 'utf-8')
msgSubject = self.secure_header_line(subject).encode(email_charset, 'replace')
msgSubject = str(Header(msgSubject, email_charset))
headerinfo['Subject'] = msgSubject
headerinfo['MIME-Version'] = '1.0'
# CC
cc_recips = filter(None, self.cc_recipients)
if cc_recips:
headerinfo['Cc'] = self._destFormat( cc_recips )
# BCC
bcc_recips = filter(None, self.bcc_recipients)
if shasattr(self, 'bccOverride') and self.getRawBccOverride():
bcc_recips = self.getBccOverride()
if bcc_recips:
headerinfo['Bcc'] = self._destFormat( bcc_recips )
for key in getattr(self, 'xinfo_headers', []):
headerinfo['X-%s' % key] = self.secure_header_line(request.get(key, 'MISSING'))
# return 3-Tuple
return (headerinfo, self.additional_headers, body)
security.declareProtected(View, 'allFieldDisplayList')
def allFieldDisplayList(self):
""" returns a DisplayList of all fields """
ret = []
for field in self.fgFieldsDisplayList():
ret.append(field)
return ret
def fieldsDisplayList(self):
""" returns display list of fields with simple values """
ret = []
foo = self.fgFieldsDisplayList(
withNone=True,
noneValue='#NONE#',
objTypes=(
'FormSelectionField',
'FormStringField',
)
)
for field in foo:
ret.append(field)
ret.append(EMAIL)
return ret
security.declareProtected(ModifyPortalContent, 'setShowFields')
def setShowFields(self, value, **kw):
""" Reorder form input to match field order """
# This wouldn't be desirable if the PickWidget
# retained order.
self.showFields = []
for field in self.fgFields(excludeServerSide=False):
id = field.getName()
if id in value:
self.showFields.append(id)
registerATCT(GetPaidFormMailerAdapter, PROJECTNAME)
def handleOrderWorkflowTransition( order, event ):
try:
if order.finance_state == event.destination and event.destination == workflow_states.order.finance.CHARGED:
annotation = IAnnotations(order.shopping_cart)
if "getpaid.formgen.mailer.adapters" in annotation:
adapters = annotation["getpaid.formgen.mailer.adapters"]
getPaidFields = _getValuesFromOrder(order)
site = zope.app.component.hooks.getSite()
for a in adapters:
annotationKey = "getpaid.formgen.mailer.%s" % a
data = annotation[annotationKey]
formFields = data['formFields']
request = data['request']
adapter = data['adapter']
adapter.__of__(site).send_form(formFields, request, getPaidFields=getPaidFields)
except:
# I catch evrything since an uncaught exception here will prevent
# the order from moving to charged even though the charge
# has gone through
logger.error("Exception sending email for order %s" % order.order_id)
def _getValuesFromOrder(order):
ret = {}
ret[NAME] = order.contact_information.name
ret[PHONE_NUMBER] = order.contact_information.phone_number
ret[EMAIL] = order.contact_information.email
ret[CONTACT_ALLOWED] = order.contact_information.marketing_preference
ret[EMAIL_PREFERENCE] = order.contact_information.email_html_format
ret[BILLING_NAME] = order.billing_address.bill_name
ret[BILLING_ORGANIZATION] = order.billing_address.bill_organization
ret[BILLING_STREET_1] = order.billing_address.bill_first_line
ret[BILLING_STREET_2] = order.billing_address.bill_second_line
ret[BILLING_CITY] = order.billing_address.bill_city
ret[BILLING_COUNTRY] = order.billing_address.bill_country
ret[BILLING_STATE] = order.billing_address.bill_state
ret[BILLING_ZIP] = order.billing_address.bill_postal_code
ret[BILLING_PHONE] = order.bill_phone_number
if order.shipping_address.ship_same_billing:
ret[SHIPPING_NAME] = order.billing_address.bill_name
ret[SHIPPING_ORGANIZATION] = order.billing_address.bill_organization
ret[SHIPPING_STREET_1] = order.billing_address.bill_first_line
ret[SHIPPING_STREET_2] = order.billing_address.bill_second_line
ret[SHIPPING_CITY] = order.billing_address.bill_city
ret[SHIPPING_COUNTRY] = order.billing_address.bill_country
ret[SHIPPING_STATE] = order.billing_address.bill_state
ret[SHIPPING_ZIP] = order.billing_address.bill_postal_code
else:
ret[SHIPPING_NAME] = order.shipping_address.ship_name
ret[SHIPPING_ORGANIZATION] = order.shipping_address.ship_organization
ret[SHIPPING_STREET_1] = order.shipping_address.ship_first_line
ret[SHIPPING_STREET_2] = order.shipping_address.ship_second_line
ret[SHIPPING_CITY] = order.shipping_address.ship_city
ret[SHIPPING_COUNTRY] = order.shipping_address.ship_country
ret[SHIPPING_STATE] = order.shipping_address.ship_state
ret[SHIPPING_ZIP] = order.shipping_address.ship_postal_code
ret[SHIPPING_SERVICE] = getShippingService(order)
ret[SHIPPING_METHOD] = getShippingMethod(order)
ret[SHIPPING_WEIGHT] = getShipmentWeight(order)
ret[ORDER_ID] = order.order_id
ret[ORDER_DATE] = order.creation_date.ctime()
# ret[ORDER_TAX_TOTAL] = order.getTaxCost()
ret[ORDER_SHIPPING_TOTAL] = order.getShippingCost()
ret[ORDER_SUB_TOTAL] = order.getSubTotalPrice()
ret[ORDER_TOTAL] = order.getTotalPrice()
ret[ORDER_TRANSACTION_ID] = order.user_payment_info_trans_id
ret[CC_NAME] = order.name_on_card
ret[CC_LAST_4] = order.user_payment_info_last4
ret[ORDER_ITEMS_ARRAY] = []
for item in order.shopping_cart.items():
itemDict = {}
itemDict[ITEM_QTY] = item[1].quantity
itemDict[ITEM_ID] = item[1].item_id
itemDict[ITEM_NAME] = item[1].name
itemDict[ITEM_PRODUCT_CODE] = item[1].product_code
itemDict[ITEM_COST] = item[1].cost
itemDict[ITEM_TOTAL_COST] = item[1].cost * item[1].quantity
itemDict[ITEM_DESC] = item[1].description
# Check to see if a discount code was applied to this item
itemDict[DISCOUNT_CODE] = ''
itemDict[DISCOUNT_TITLE] = ''
itemDict[DISCOUNT_TOTAL] = ''
annotation = IAnnotations(item[1])
if "getpaid.discount.code" in annotation:
itemDict[DISCOUNT_CODE] = annotation["getpaid.discount.code"]
itemDict[DISCOUNT_TITLE] = annotation["getpaid.discount.code.title"]
itemDict[DISCOUNT_TOTAL] = annotation["getpaid.discount.code.discount"]
ret[ORDER_ITEMS_ARRAY].append(itemDict)
return ret
def getShippingService(order):
if not hasattr(order,"shipping_service"):
return None
infos = order.shipping_service
if infos:
return infos
def getShippingMethod(order):
# check the traversable wrrapper
if not IShippableOrder.providedBy( order ):
return None
service = zope.component.queryUtility( IShippingRateService,
order.shipping_service )
# play nice if the a shipping method is removed from the store
if not service:
return None
return service.getMethodName( order.shipping_method )
def getShipmentWeight(order):
"""
Lets return the weight in lbs for the moment
"""
# check the traversable wrrapper
if not IShippableOrder.providedBy( order ):
return None
totalShipmentWeight = 0
for eachProduct in order.shopping_cart.values():
if IShippableLineItem.providedBy( eachProduct ):
weightValue = eachProduct.weight * eachProduct.quantity
totalShipmentWeight += weightValue
return totalShipmentWeight
NAME = u'Name'
PHONE_NUMBER = u'Phone Number'
EMAIL = u'Email'
CONTACT_ALLOWED = u'Contact Allowed'
EMAIL_PREFERENCE = u'Email Format Preference'
BILLING_NAME = u'Billing Address Name'
BILLING_ORGANIZATION = u'Billing Organization'
BILLING_STREET_1 = u'Billing Address Street 1'
BILLING_STREET_2 = u'Billing Address Street 2'
BILLING_CITY = u'Billing Address City'
BILLING_COUNTRY = u'Billing Address Country'
BILLING_STATE = u'Billing Address State'
BILLING_ZIP = u'Billing Address Zip'
BILLING_PHONE = u'Billing Phone Number'
SHIPPING_NAME = u'Shipping Address Name'
SHIPPING_ORGANIZATION = u'Shipping Organization'
SHIPPING_STREET_1 = u'Shipping Address Street 1'
SHIPPING_STREET_2 = u'Shipping Address Street 2'
SHIPPING_CITY = u'Shipping Address City'
SHIPPING_COUNTRY = u'Shipping Address Country'
SHIPPING_STATE = u'Shipping Address State'
SHIPPING_ZIP = u'Shipping Address Zip'
SHIPPING_SERVICE = u'Shipping Service'
SHIPPING_METHOD = u'Shipping Method'
SHIPPING_WEIGHT = u'Shipping Weight'
ORDER_ID = u'Order Id'
ORDER_DATE = u'Order Creation Date'
ORDER_TAX_TOTAL = u'Tax Total'
ORDER_SHIPPING_TOTAL = u'Shipping Total'
ORDER_SUB_TOTAL = u'Order Subtotal'
ORDER_TOTAL = u'Order Total'
ORDER_TRANSACTION_ID = u'Order Transaction Id'
CC_NAME = u'Cardholder Name'
CC_LAST_4 = u'CC Last 4'
ORDER_ITEMS_ARRAY = u'Items'
DISCOUNT_CODE = u'Discount Code'
DISCOUNT_TITLE = u'Discount Title'
DISCOUNT_TOTAL = u'Discount Total'
ITEM_QTY = u'Line Item Quantity'
ITEM_ID = u'Line Item Id'
ITEM_NAME = u'Line Item Name'
ITEM_PRODUCT_CODE = u'Line Item Product Code'
ITEM_COST = u'Line Item Item Cost'
ITEM_TOTAL_COST = u'Total Line Item Cost'
ITEM_DESC = u'Line Item Item Description'
GetPaidFields = (
NAME,
PHONE_NUMBER,
EMAIL,
CONTACT_ALLOWED,
EMAIL_PREFERENCE,
BILLING_NAME,
BILLING_ORGANIZATION,
BILLING_STREET_1,
BILLING_STREET_2,
BILLING_CITY,
BILLING_COUNTRY,
BILLING_STATE,
BILLING_ZIP,
SHIPPING_NAME,
SHIPPING_ORGANIZATION,
SHIPPING_STREET_1,
SHIPPING_STREET_2,
SHIPPING_CITY,
SHIPPING_COUNTRY,
SHIPPING_STATE,
SHIPPING_ZIP,
SHIPPING_SERVICE,
SHIPPING_METHOD,
SHIPPING_WEIGHT,
ORDER_ID,
ORDER_DATE,
ORDER_TAX_TOTAL,
ORDER_SHIPPING_TOTAL,
ORDER_SUB_TOTAL,
ORDER_TOTAL,
ORDER_TRANSACTION_ID,
DISCOUNT_CODE,
DISCOUNT_TITLE,
DISCOUNT_TOTAL,
CC_LAST_4,
ORDER_ITEMS_ARRAY,
ITEM_QTY,
ITEM_ID,
ITEM_NAME,
ITEM_PRODUCT_CODE,
ITEM_COST,
ITEM_TOTAL_COST,
ITEM_DESC,
)
DEFAULT_MAILTEMPLATE_BODY = \
"""<html xmlns="http://www.w3.org/1999/xhtml">
<head><title></title></head>
<body>
<p tal:content="here/getBody_pre | nothing" />
<dl>
<tal:block repeat="field options/formFields">
<dt tal:content="python:field[0]"/>
<dt tal:content="python:field[1]"/>
</tal:block>
</dl>
<tal:block define="field options/getPaidFields">
<div>
<div style="float:left; width:30%">
<fieldset>
<legend> Billing Address </legend>
<span tal:content="python:field['Billing Address Name']">Name</span><br />
<span tal:content="python:field['Billing Organization']">Org</span><br />
<span tal:content="python:field['Billing Address Street 1']">Street 1</span><br />
<span tal:content="python:field['Billing Address Street 2']">Street 2</span><br />
<span tal:content="python:field['Billing Address City']">City</span><br />
<span tal:content="python:field['Billing Address Country']">Country</span><br />
<span tal:content="python:field['Billing Address State']">State</span><br />
<span tal:content="python:field['Billing Address Zip']">Zip</span><br />
</fieldset>
</div>
<div style="float: left; padding-left: 3em; width: 30%;">
<fieldset>
<legend> Mailing Address </legend>
<span tal:content="python:field['Shipping Address Name']">Name</span><br />
<span tal:content="python:field['Shipping Organization']">Org</span><br />
<span tal:content="python:field['Shipping Address Street 1']">Street 1</span><br />
<span tal:content="python:field['Shipping Address Street 2']">Street 2</span><br />
<span tal:content="python:field['Shipping Address City']">City</span><br />
<span tal:content="python:field['Shipping Address Country']">Country</span><br />
<span tal:content="python:field['Shipping Address State']">State</span><br />
<span tal:content="python:field['Shipping Address Zip']">Zip</span><br />
</fieldset>
</div>
</div>
<div style="clear:both;"><!-- --></div>
<div>
<fieldset>
<legend> Contact Information </legend>
Email: <span tal:content="python:field['Email']">Email</span><br />
Billing Phone: <span tal:content="python:field['Billing Phone Number']">Phone</span><br />
Phone: <span tal:content="python:field['Phone Number']">Phone</span><br />
</fieldset>
</div>
<div style="clear:both;"><!-- --></div>
<div>
<fieldset>
<legend> Shipping Information </legend>
<table class="listing">
<tbody>
<tr>
<td>
Shipping Service
</td>
<td>
<span tal:content="python:field['Shipping Service']">Shipping Service</span>
</td>
</tr>
<tr>
<td>
Shipping Method
</td>
<td>
<span tal:content="python:field['Shipping Method']">Shipping Method</span>
</td>
</tr>
<tr>
<td>
Shipping Weight
</td>
<td>
<span tal:content="python:field['Shipping Weight']">Shipping Weight</span>
</td>
</tr>
</tbody>
</table>
</fieldset>
</div>
<div style="clear:both;"><!-- --></div>
<div class="cart-listing">
<fieldset>
<legend> Shopping Cart </legend>
<table class="listing">
<thead>
<tr>
<th>
Quantity
</th>
<th>
Name
</th>
<th>
Price
</th>
<th>
Total
</th>
</tr>
</thead>
<tbody>
<tal:block tal:repeat="item python:field['Items']">
<tr>
<td>
<span tal:content="python:item['Line Item Quantity']">1</span>
</td>
<td>
<span tal:content="python:item['Line Item Name']">Name</span>
</td>
<td>
<span tal:content="python:item['Line Item Item Cost']">Price</span>
</td>
<td>
<span tal:content="python:item['Total Line Item Cost']">Total Cost</span>
</td>
</tr>
</tal:block>
</tbody>
</table>
<div class="getpaid-totals">
<table class="listing">
<tr>
<th>SubTotal</th>
<td><span tal:content="python:field['Order Subtotal']">Subtotal</span></td>
</tr>
<tr>
<th>Shipping</th>
<td><span tal:content="python:field['Shipping Total']">Shipping</span></td>
</tr>
<!--
<tr>
<th>Tax</th>
<td><span tal:content="python:field['Tax']">Tax</span></td>
</tr>
-->
<tr>
<th>Total</th>
<td><span tal:content="python:field['Order Total']">Order Total</span></td>
</tr>
</table>
</div>
</fieldset>
</div>
</tal:block>
<p tal:content="here/getBody_post | nothing" />
<pre tal:content="here/getBody_footer | nothing" />
</body>
</html>
"""