diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index 10789d3743..b67497ec1b 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -41,6 +41,7 @@ from shoppingcart.models import (
RegistrationCodeRedemption, Order, CouponRedemption,
PaidCourseRegistration, Coupon, Invoice, CourseRegistrationCode
)
+from shoppingcart.pdf import PDFInvoice
from student.models import (
CourseEnrollment, CourseEnrollmentAllowed, NonExistentCourseError
)
@@ -3296,6 +3297,25 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase):
self.assertTrue(body.startswith(EXPECTED_CSV_HEADER))
self.assertEqual(len(body.split('\n')), 11)
+ def test_pdf_file_throws_exception(self):
+ """
+ test to mock the pdf file generation throws an exception
+ when generating registration codes.
+ """
+ generate_code_url = reverse(
+ 'generate_registration_codes', kwargs={'course_id': self.course.id.to_deprecated_string()}
+ )
+ data = {
+ 'total_registration_codes': 9, 'company_name': 'Group Alpha', 'company_contact_name': 'Test@company.com',
+ 'company_contact_email': 'Test@company.com', 'sale_price': 122.45, 'recipient_name': 'Test123',
+ 'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street', 'address_line_2': '',
+ 'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '',
+ 'customer_reference_number': '123A23F', 'internal_reference': '', 'invoice': ''
+ }
+ with patch.object(PDFInvoice, 'generate_pdf', side_effect=Exception):
+ response = self.client.post(generate_code_url, data)
+ self.assertEqual(response.status_code, 200, response.content)
+
def test_get_codes_with_sale_invoice(self):
"""
Test to generate a response of all the course registration codes
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index a2270e17a9..dc0291e9f4 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -1231,6 +1231,12 @@ def generate_registration_codes(request, course_id):
dashboard=reverse('dashboard')
)
+ try:
+ pdf_file = sale_invoice.generate_pdf_invoice(course, course_price, int(quantity), float(sale_price))
+ except Exception: # pylint: disable=broad-except
+ log.exception('Exception at creating pdf file.')
+ pdf_file = None
+
from_address = microsite.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
context = {
'invoice': sale_invoice,
@@ -1277,6 +1283,11 @@ def generate_registration_codes(request, course_id):
email.to = [recipient]
email.attach(u'RegistrationCodes.csv', csv_file.getvalue(), 'text/csv')
email.attach(u'Invoice.txt', invoice_attachment, 'text/plain')
+ if pdf_file is not None:
+ email.attach(u'Invoice.pdf', pdf_file.getvalue(), 'application/pdf')
+ else:
+ file_buffer = StringIO.StringIO(_('pdf download unavailable right now, please contact support.'))
+ email.attach(u'pdf_unavailable.txt', file_buffer.getvalue(), 'text/plain')
email.send()
return registration_codes_csv("Registration_Codes.csv", registration_codes)
diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py
index 4f592bcefb..4ce199aaff 100644
--- a/lms/djangoapps/shoppingcart/models.py
+++ b/lms/djangoapps/shoppingcart/models.py
@@ -5,12 +5,12 @@ from datetime import datetime
from datetime import timedelta
from decimal import Decimal
import analytics
+from io import BytesIO
import pytz
import logging
import smtplib
import StringIO
import csv
-from courseware.courses import get_course_by_id
from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
from django.dispatch import receiver
from django.db import models
@@ -25,19 +25,17 @@ from django.core.urlresolvers import reverse
from model_utils.managers import InheritanceManager
from model_utils.models import TimeStampedModel
from django.core.mail.message import EmailMessage
-
from xmodule.modulestore.django import modulestore
+from eventtracking import tracker
+from courseware.courses import get_course_by_id
from config_models.models import ConfigurationModel
from course_modes.models import CourseMode
from edxmako.shortcuts import render_to_string
from student.models import CourseEnrollment, UNENROLL_DONE
-from eventtracking import tracker
from util.query import use_read_replica_if_available
from xmodule_django.models import CourseKeyField
-
from verify_student.models import SoftwareSecurePhotoVerification
-
from .exceptions import (
InvalidCartItem,
PurchasedCallbackException,
@@ -49,8 +47,9 @@ from .exceptions import (
UnexpectedOrderItemStatus,
ItemNotFoundInCartException
)
-
from microsite_configuration import microsite
+from shoppingcart.pdf import PDFInvoice
+
log = logging.getLogger("shoppingcart")
@@ -288,6 +287,41 @@ class Order(models.Model):
self.save()
return old_to_new_id_map
+ def generate_pdf_receipt(self, order_items):
+ """
+ Generates the pdf receipt for the given order_items
+ and returns the pdf_buffer.
+ """
+ items_data = []
+ for item in order_items:
+ if item.list_price is not None:
+ discount_price = item.list_price - item.unit_cost
+ price = item.list_price
+ else:
+ discount_price = 0
+ price = item.unit_cost
+
+ item_total = item.qty * item.unit_cost
+ items_data.append({
+ 'item_description': item.pdf_receipt_display_name,
+ 'quantity': item.qty,
+ 'list_price': price,
+ 'discount': discount_price,
+ 'item_total': item_total
+ })
+ pdf_buffer = BytesIO()
+
+ PDFInvoice(
+ items_data=items_data,
+ item_id=str(self.id), # pylint: disable=no-member
+ date=self.purchase_time,
+ is_invoice=False,
+ total_cost=self.total_cost,
+ payment_received=self.total_cost,
+ balance=0
+ ).generate_pdf(pdf_buffer)
+ return pdf_buffer
+
def generate_registration_codes_csv(self, orderitems, site_name):
"""
this function generates the csv file
@@ -308,7 +342,7 @@ class Order(models.Model):
return csv_file, course_info
- def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, site_name, courses_info):
+ def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, pdf_file, site_name, courses_info):
"""
send confirmation e-mail
"""
@@ -370,6 +404,11 @@ class Order(models.Model):
if csv_file:
email.attach(u'RegistrationCodesRedemptionUrls.csv', csv_file.getvalue(), 'text/csv')
+ if pdf_file is not None:
+ email.attach(u'Receipt.pdf', pdf_file.getvalue(), 'application/pdf')
+ else:
+ file_buffer = StringIO.StringIO(_('pdf download unavailable right now, please contact support.'))
+ email.attach(u'pdf_not_available.txt', file_buffer.getvalue(), 'text/plain')
email.send()
except (smtplib.SMTPException, BotoServerError): # sadly need to handle diff. mail backends individually
log.error('Failed sending confirmation e-mail for order %d', self.id) # pylint: disable=no-member
@@ -436,7 +475,16 @@ class Order(models.Model):
#
csv_file, courses_info = self.generate_registration_codes_csv(orderitems, site_name)
- self.send_confirmation_emails(orderitems, self.order_type == OrderTypes.BUSINESS, csv_file, site_name, courses_info)
+ try:
+ pdf_file = self.generate_pdf_receipt(orderitems)
+ except Exception: # pylint: disable=broad-except
+ log.exception('Exception at creating pdf file.')
+ pdf_file = None
+
+ self.send_confirmation_emails(
+ orderitems, self.order_type == OrderTypes.BUSINESS,
+ csv_file, pdf_file, site_name, courses_info
+ )
self._emit_order_event('Completed Order', orderitems)
def refund(self):
@@ -679,6 +727,22 @@ class OrderItem(TimeStampedModel):
"""
return ''
+ @property
+ def pdf_receipt_display_name(self):
+ """
+ How to display this item on a PDF printed receipt file.
+ This can be overridden by the subclasses of OrderItem
+ """
+ course_key = getattr(self, 'course_id', None)
+ if course_key:
+ course = get_course_by_id(course_key, depth=0)
+ return course.display_name
+ else:
+ raise Exception(
+ "Not Implemented. OrderItems that are not Course specific should have"
+ " a overridden pdf_receipt_display_name property"
+ )
+
def analytics_data(self):
"""Simple function used to construct analytics data for the OrderItem.
@@ -733,6 +797,33 @@ class Invoice(models.Model):
customer_reference_number = models.CharField(max_length=63, null=True)
is_valid = models.BooleanField(default=True)
+ def generate_pdf_invoice(self, course, course_price, quantity, sale_price):
+ """
+ Generates the pdf invoice for the given course
+ and returns the pdf_buffer.
+ """
+ discount_per_item = float(course_price) - sale_price / quantity
+ list_price = course_price - discount_per_item
+ items_data = [{
+ 'item_description': course.display_name,
+ 'quantity': quantity,
+ 'list_price': list_price,
+ 'discount': discount_per_item,
+ 'item_total': quantity * list_price
+ }]
+ pdf_buffer = BytesIO()
+ PDFInvoice(
+ items_data=items_data,
+ item_id=str(self.id), # pylint: disable=no-member
+ date=datetime.now(pytz.utc),
+ is_invoice=True,
+ total_cost=float(self.total_amount),
+ payment_received=0,
+ balance=float(self.total_amount)
+ ).generate_pdf(pdf_buffer)
+
+ return pdf_buffer
+
class CourseRegistrationCode(models.Model):
"""
@@ -1606,3 +1697,10 @@ class Donation(OrderItem):
data['name'] = settings.PLATFORM_NAME
data['category'] = settings.PLATFORM_NAME
return data
+
+ @property
+ def pdf_receipt_display_name(self):
+ """
+ How to display this item on a PDF printed receipt file.
+ """
+ return self._line_item_description(course_id=self.course_id)
diff --git a/lms/djangoapps/shoppingcart/pdf.py b/lms/djangoapps/shoppingcart/pdf.py
new file mode 100644
index 0000000000..16de5fd0d6
--- /dev/null
+++ b/lms/djangoapps/shoppingcart/pdf.py
@@ -0,0 +1,468 @@
+"""
+Template for PDF Receipt/Invoice Generation
+"""
+from PIL import Image
+import logging
+from reportlab.lib import colors
+from django.conf import settings
+from django.utils.translation import ugettext as _
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.lib.pagesizes import letter
+from reportlab.lib.units import mm
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.platypus import Paragraph
+from reportlab.platypus.tables import Table, TableStyle
+from microsite_configuration import microsite
+from xmodule.modulestore.django import ModuleI18nService
+
+log = logging.getLogger("PDF Generation")
+
+
+class NumberedCanvas(Canvas): # pylint: disable=abstract-method
+ """
+ Canvas child class with auto page-numbering.
+ """
+ def __init__(self, *args, **kwargs):
+ """
+ __init__
+ """
+ Canvas.__init__(self, *args, **kwargs)
+ self._saved_page_states = []
+
+ def insert_page_break(self):
+ """
+ Starts a new page.
+ """
+ self._saved_page_states.append(dict(self.__dict__))
+ self._startPage()
+
+ def current_page_count(self):
+ """
+ Returns the page count in the current pdf document.
+ """
+ return len(self._saved_page_states) + 1
+
+ def save(self):
+ """
+ Adds page numbering to each page (page x of y)
+ """
+ num_pages = len(self._saved_page_states)
+ for state in self._saved_page_states:
+ self.__dict__.update(state)
+ if num_pages > 1:
+ self.draw_page_number(num_pages)
+ Canvas.showPage(self)
+ Canvas.save(self)
+
+ def draw_page_number(self, page_count):
+ """
+ Draws the String "Page x of y" at the bottom right of the document.
+ """
+ self.setFontSize(7)
+ self.drawRightString(
+ 200 * mm,
+ 12 * mm,
+ _("Page {page_number} of {page_count}").format(page_number=self._pageNumber, page_count=page_count)
+ )
+
+
+class PDFInvoice(object):
+ """
+ PDF Generation Class
+ """
+ def __init__(self, items_data, item_id, date, is_invoice, total_cost, payment_received, balance):
+ """
+ Accepts the following positional arguments
+
+ items_data - A list having the following items for each row.
+ item_description - String
+ quantity - Integer
+ list_price - float
+ discount - float
+ item_total - float
+ id - String
+ date - datetime
+ is_invoice - boolean - True (for invoice) or False (for Receipt)
+ total_cost - float
+ payment_received - float
+ balance - float
+ """
+
+ # From settings
+ self.currency = settings.PAID_COURSE_REGISTRATION_CURRENCY[1]
+ self.logo_path = microsite.get_value("PDF_RECEIPT_LOGO_PATH", settings.PDF_RECEIPT_LOGO_PATH)
+ self.cobrand_logo_path = microsite.get_value(
+ "PDF_RECEIPT_COBRAND_LOGO_PATH", settings.PDF_RECEIPT_COBRAND_LOGO_PATH
+ )
+ self.tax_label = microsite.get_value("PDF_RECEIPT_TAX_ID_LABEL", settings.PDF_RECEIPT_TAX_ID_LABEL)
+ self.tax_id = microsite.get_value("PDF_RECEIPT_TAX_ID", settings.PDF_RECEIPT_TAX_ID)
+ self.footer_text = microsite.get_value("PDF_RECEIPT_FOOTER_TEXT", settings.PDF_RECEIPT_FOOTER_TEXT)
+ self.disclaimer_text = microsite.get_value("PDF_RECEIPT_DISCLAIMER_TEXT", settings.PDF_RECEIPT_DISCLAIMER_TEXT)
+ self.billing_address_text = microsite.get_value(
+ "PDF_RECEIPT_BILLING_ADDRESS", settings.PDF_RECEIPT_BILLING_ADDRESS
+ )
+ self.terms_conditions_text = microsite.get_value(
+ "PDF_RECEIPT_TERMS_AND_CONDITIONS", settings.PDF_RECEIPT_TERMS_AND_CONDITIONS
+ )
+ self.brand_logo_height = microsite.get_value(
+ "PDF_RECEIPT_LOGO_HEIGHT_MM", settings.PDF_RECEIPT_LOGO_HEIGHT_MM
+ ) * mm
+ self.cobrand_logo_height = microsite.get_value(
+ "PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM", settings.PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM
+ ) * mm
+
+ # From Context
+ self.items_data = items_data
+ self.item_id = item_id
+ self.date = ModuleI18nService().strftime(date, 'SHORT_DATE')
+ self.is_invoice = is_invoice
+ self.total_cost = '{currency}{amount:.2f}'.format(currency=self.currency, amount=total_cost)
+ self.payment_received = '{currency}{amount:.2f}'.format(currency=self.currency, amount=payment_received)
+ self.balance = '{currency}{amount:.2f}'.format(currency=self.currency, amount=balance)
+
+ # initialize the pdf variables
+ self.margin = 15 * mm
+ self.page_width = letter[0]
+ self.page_height = letter[1]
+ self.min_clearance = 3 * mm
+ self.second_page_available_height = ''
+ self.second_page_start_y_pos = ''
+ self.first_page_available_height = ''
+ self.pdf = None
+
+ def is_on_first_page(self):
+ """
+ Returns True if it's the first page of the pdf, False otherwise.
+ """
+ return self.pdf.current_page_count() == 1
+
+ def generate_pdf(self, file_buffer):
+ """
+ Takes in a buffer and puts the generated pdf into that buffer.
+ """
+ self.pdf = NumberedCanvas(file_buffer, pagesize=letter)
+
+ self.draw_border()
+ y_pos = self.draw_logos()
+ self.second_page_available_height = y_pos - self.margin - self.min_clearance
+ self.second_page_start_y_pos = y_pos
+
+ y_pos = self.draw_title(y_pos)
+ self.first_page_available_height = y_pos - self.margin - self.min_clearance
+
+ y_pos = self.draw_course_info(y_pos)
+ y_pos = self.draw_totals(y_pos)
+ self.draw_footer(y_pos)
+
+ self.pdf.insert_page_break()
+ self.pdf.save()
+
+ def draw_border(self):
+ """
+ Draws a big border around the page leaving a margin of 15 mm on each side.
+ """
+ self.pdf.setStrokeColorRGB(0.5, 0.5, 0.5)
+ self.pdf.setLineWidth(0.353 * mm)
+
+ self.pdf.rect(self.margin, self.margin,
+ self.page_width - (self.margin * 2), self.page_height - (self.margin * 2),
+ stroke=True, fill=False)
+
+ @staticmethod
+ def load_image(img_path):
+ """
+ Loads an image given a path. An absolute path is assumed.
+ If the path points to an image file, it loads and returns the Image object, None otherwise.
+ """
+ try:
+ img = Image.open(img_path)
+ except IOError, ex:
+ log.exception('Pdf unable to open the image file: %s', str(ex))
+ img = None
+
+ return img
+
+ def draw_logos(self):
+ """
+ Draws logos.
+ """
+ horizontal_padding_from_border = self.margin + 9 * mm
+ vertical_padding_from_border = 11 * mm
+ img_y_pos = self.page_height - (
+ self.margin + vertical_padding_from_border + max(self.cobrand_logo_height, self.brand_logo_height)
+ )
+
+ # Left-Aligned cobrand logo
+ cobrand_img = self.load_image(self.cobrand_logo_path)
+ if cobrand_img:
+ img_width = float(cobrand_img.size[0]) / (float(cobrand_img.size[1]) / self.cobrand_logo_height)
+ self.pdf.drawImage(cobrand_img.filename, horizontal_padding_from_border, img_y_pos, img_width,
+ self.cobrand_logo_height, mask='auto')
+
+ # Right aligned brand logo
+ logo_img = self.load_image(self.logo_path)
+ if logo_img:
+ img_width = float(logo_img.size[0]) / (float(logo_img.size[1]) / self.brand_logo_height)
+ self.pdf.drawImage(
+ logo_img.filename,
+ self.page_width - (horizontal_padding_from_border + img_width),
+ img_y_pos,
+ img_width,
+ self.brand_logo_height,
+ mask='auto'
+ )
+
+ return img_y_pos - self.min_clearance
+
+ def draw_title(self, y_pos):
+ """
+ Draws the title, order/receipt ID and the date.
+ """
+ if self.is_invoice:
+ title = (_('Invoice'))
+ id_label = (_('Invoice'))
+ else:
+ title = (_('Receipt'))
+ id_label = (_('Order'))
+
+ # Draw Title "RECEIPT" OR "INVOICE"
+ vertical_padding = 5 * mm
+ horizontal_padding_from_border = self.margin + 9 * mm
+ font_size = 21
+ self.pdf.setFontSize(font_size)
+ self.pdf.drawString(horizontal_padding_from_border, y_pos - vertical_padding - font_size / 2, title)
+ y_pos = y_pos - vertical_padding - font_size / 2 - self.min_clearance
+
+ horizontal_padding_from_border = self.margin + 11 * mm
+ font_size = 12
+ self.pdf.setFontSize(font_size)
+ y_pos = y_pos - font_size / 2 - vertical_padding
+ # Draw Order/Invoice No.
+ self.pdf.drawString(horizontal_padding_from_border, y_pos,
+ _(u'{id_label} # {item_id}'.format(id_label=id_label, item_id=self.item_id)))
+ y_pos = y_pos - font_size / 2 - vertical_padding
+ # Draw Date
+ self.pdf.drawString(
+ horizontal_padding_from_border, y_pos, _(u'Date: {date}').format(date=self.date)
+ )
+
+ return y_pos - self.min_clearance
+
+ def draw_course_info(self, y_pos):
+ """
+ Draws the main table containing the data items.
+ """
+ course_items_data = [
+ ['', (_('Description')), (_('Quantity')), (_('List Price\nper item')), (_('Discount\nper item')),
+ (_('Amount')), '']
+ ]
+ for row_item in self.items_data:
+ course_items_data.append([
+ '',
+ Paragraph(row_item['item_description'], getSampleStyleSheet()['Normal']),
+ row_item['quantity'],
+ '{currency}{list_price:.2f}'.format(list_price=row_item['list_price'], currency=self.currency),
+ '{currency}{discount:.2f}'.format(discount=row_item['discount'], currency=self.currency),
+ '{currency}{item_total:.2f}'.format(item_total=row_item['item_total'], currency=self.currency),
+ ''
+ ])
+
+ padding_width = 7 * mm
+ desc_col_width = 60 * mm
+ qty_col_width = 26 * mm
+ list_price_col_width = 21 * mm
+ discount_col_width = 21 * mm
+ amount_col_width = 40 * mm
+ course_items_table = Table(
+ course_items_data,
+ [
+ padding_width,
+ desc_col_width,
+ qty_col_width,
+ list_price_col_width,
+ discount_col_width,
+ amount_col_width,
+ padding_width
+ ],
+ splitByRow=1,
+ repeatRows=1
+ )
+
+ course_items_table.setStyle(TableStyle([
+ #List Price, Discount, Amount data items
+ ('ALIGN', (3, 1), (5, -1), 'RIGHT'),
+
+ # Amount header
+ ('ALIGN', (5, 0), (5, 0), 'RIGHT'),
+
+ # Amount column (header + data items)
+ ('RIGHTPADDING', (5, 0), (5, -1), 7 * mm),
+
+ # Quantity, List Price, Discount header
+ ('ALIGN', (2, 0), (4, 0), 'CENTER'),
+
+ # Description header
+ ('ALIGN', (1, 0), (1, -1), 'LEFT'),
+
+ # Quantity data items
+ ('ALIGN', (2, 1), (2, -1), 'CENTER'),
+
+ # Lines below the header and at the end of the table.
+ ('LINEBELOW', (0, 0), (-1, 0), 1.00, '#cccccc'),
+ ('LINEBELOW', (0, -1), (-1, -1), 1.00, '#cccccc'),
+
+ # Innergrid around the data rows.
+ ('INNERGRID', (1, 1), (-2, -1), 0.50, '#cccccc'),
+
+ # Entire table
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
+ ('TOPPADDING', (0, 0), (-1, -1), 2 * mm),
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 2 * mm),
+ ('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
+ ]))
+ rendered_width, rendered_height = course_items_table.wrap(0, 0)
+ table_left_padding = (self.page_width - rendered_width) / 2
+
+ split_tables = course_items_table.split(0, self.first_page_available_height)
+ if len(split_tables) > 1:
+ # The entire Table won't fit in the available space and requires splitting.
+ # Draw the part that can fit, start a new page
+ # and repeat the process with the rest of the table.
+ split_table = split_tables[0]
+ __, rendered_height = split_table.wrap(0, 0)
+ split_table.drawOn(self.pdf, table_left_padding, y_pos - rendered_height)
+
+ self.prepare_new_page()
+ split_tables = split_tables[1].split(0, self.second_page_available_height)
+ while len(split_tables) > 1:
+ split_table = split_tables[0]
+ __, rendered_height = split_table.wrap(0, 0)
+ split_table.drawOn(self.pdf, table_left_padding, self.second_page_start_y_pos - rendered_height)
+
+ self.prepare_new_page()
+ split_tables = split_tables[1].split(0, self.second_page_available_height)
+ split_table = split_tables[0]
+ __, rendered_height = split_table.wrap(0, 0)
+ split_table.drawOn(self.pdf, table_left_padding, self.second_page_start_y_pos - rendered_height)
+ else:
+ # Table will fit without the need for splitting.
+ course_items_table.drawOn(self.pdf, table_left_padding, y_pos - rendered_height)
+
+ if not self.is_on_first_page():
+ y_pos = self.second_page_start_y_pos
+
+ return y_pos - rendered_height - self.min_clearance
+
+ def prepare_new_page(self):
+ """
+ Inserts a new page and includes the border and the logos.
+ """
+ self.pdf.insert_page_break()
+ self.draw_border()
+ y_pos = self.draw_logos()
+ return y_pos
+
+ def draw_totals(self, y_pos):
+ """
+ Draws the boxes containing the totals and the tax id.
+ """
+ totals_data = [
+ [(_('Total')), self.total_cost],
+ [(_('Payment Received')), self.payment_received],
+ [(_('Balance')), self.balance],
+ ['', '{tax_label}: {tax_id}'.format(tax_label=self.tax_label, tax_id=self.tax_id)]
+ ]
+
+ heights = 8 * mm
+ totals_table = Table(totals_data, 40 * mm, heights)
+
+ totals_table.setStyle(TableStyle([
+ # Styling for the totals table.
+ ('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
+ ('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
+
+ # Styling for the Amounts cells
+ ('RIGHTPADDING', (-1, 0), (-1, -2), 7 * mm),
+ ('GRID', (-1, 0), (-1, -2), 3.0, colors.white),
+ ('BACKGROUND', (-1, 0), (-1, -2), '#EEEEEE'),
+ ]))
+
+ __, rendered_height = totals_table.wrap(0, 0)
+
+ left_padding = 97 * mm
+ if y_pos - (self.margin + self.min_clearance) <= rendered_height:
+ # if space left on page is smaller than the rendered height, render the table on the next page.
+ self.prepare_new_page()
+ totals_table.drawOn(self.pdf, self.margin + left_padding, self.second_page_start_y_pos - rendered_height)
+ return self.second_page_start_y_pos - rendered_height - self.min_clearance
+ else:
+ totals_table.drawOn(self.pdf, self.margin + left_padding, y_pos - rendered_height)
+ return y_pos - rendered_height - self.min_clearance
+
+ def draw_footer(self, y_pos):
+ """
+ Draws the footer.
+ """
+
+ para_style = getSampleStyleSheet()['Normal']
+ para_style.fontSize = 8
+
+ footer_para = Paragraph(self.footer_text.replace("\n", "
"), para_style)
+ disclaimer_para = Paragraph(self.disclaimer_text.replace("\n", "
"), para_style)
+ billing_address_para = Paragraph(self.billing_address_text.replace("\n", "
"), para_style)
+
+ footer_data = [
+ ['', footer_para],
+ [(_('Billing Address')), ''],
+ ['', billing_address_para],
+ [(_('Disclaimer')), ''],
+ ['', disclaimer_para]
+ ]
+
+ footer_style = [
+ # Styling for the entire footer table.
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
+ ('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
+ ('FONTSIZE', (0, 0), (-1, -1), 9),
+ ('TEXTCOLOR', (0, 0), (-1, -1), '#AAAAAA'),
+
+ # Billing Address Header styling
+ ('LEFTPADDING', (0, 1), (0, 1), 5 * mm),
+
+ # Disclaimer Header styling
+ ('LEFTPADDING', (0, 3), (0, 3), 5 * mm),
+ ('TOPPADDING', (0, 3), (0, 3), 2 * mm),
+
+ # Footer Body styling
+ # ('BACKGROUND', (1, 0), (1, 0), '#EEEEEE'),
+
+ # Billing Address Body styling
+ ('BACKGROUND', (1, 2), (1, 2), '#EEEEEE'),
+
+ # Disclaimer Body styling
+ ('BACKGROUND', (1, 4), (1, 4), '#EEEEEE'),
+ ]
+
+ if self.is_invoice:
+ terms_conditions_para = Paragraph(self.terms_conditions_text.replace("\n", "
"), para_style)
+ footer_data.append([(_('TERMS AND CONDITIONS')), ''])
+ footer_data.append(['', terms_conditions_para])
+
+ # TERMS AND CONDITIONS header styling
+ footer_style.append(('LEFTPADDING', (0, 5), (0, 5), 5 * mm))
+ footer_style.append(('TOPPADDING', (0, 5), (0, 5), 2 * mm))
+
+ # TERMS AND CONDITIONS body styling
+ footer_style.append(('BACKGROUND', (1, 6), (1, 6), '#EEEEEE'))
+
+ footer_table = Table(footer_data, [5 * mm, 176 * mm])
+
+ footer_table.setStyle(TableStyle(footer_style))
+ __, rendered_height = footer_table.wrap(0, 0)
+
+ if y_pos - (self.margin + self.min_clearance) <= rendered_height:
+ self.prepare_new_page()
+
+ footer_table.drawOn(self.pdf, self.margin, self.margin + 5 * mm)
diff --git a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py
index 34c308f5b3..c92aa698ab 100644
--- a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py
+++ b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py
@@ -118,11 +118,27 @@ class CyberSource2Test(TestCase):
# Check the signature
self.assertEqual(params['signature'], self._signature(params))
+ # We patch the purchased callback because
+ # we're using the OrderItem base class, which throws an exception
+ # when item doest not have a course id associated
+ @patch.object(OrderItem, 'purchased_callback')
+ def test_process_payment_raises_exception(self, purchased_callback): # pylint: disable=unused-argument
+ self.order.clear()
+ OrderItem.objects.create(
+ order=self.order,
+ user=self.user,
+ unit_cost=self.COST,
+ line_cost=self.COST,
+ )
+ params = self._signed_callback_params(self.order.id, self.COST, self.COST)
+ process_postpay_callback(params)
+
# We patch the purchased callback because
# (a) we're using the OrderItem base class, which doesn't implement this method, and
# (b) we want to verify that the method gets called on success.
@patch.object(OrderItem, 'purchased_callback')
- def test_process_payment_success(self, purchased_callback):
+ @patch.object(OrderItem, 'pdf_receipt_display_name')
+ def test_process_payment_success(self, pdf_receipt_display_name, purchased_callback): # pylint: disable=unused-argument
# Simulate a callback from CyberSource indicating that payment was successful
params = self._signed_callback_params(self.order.id, self.COST, self.COST)
result = process_postpay_callback(params)
@@ -201,7 +217,8 @@ class CyberSource2Test(TestCase):
self.assertIn(u"you have cancelled this transaction", result['error_html'])
@patch.object(OrderItem, 'purchased_callback')
- def test_process_no_credit_card_digits(self, callback):
+ @patch.object(OrderItem, 'pdf_receipt_display_name')
+ def test_process_no_credit_card_digits(self, pdf_receipt_display_name, purchased_callback): # pylint: disable=unused-argument
# Use a credit card number with no digits provided
params = self._signed_callback_params(
self.order.id, self.COST, self.COST,
@@ -238,7 +255,8 @@ class CyberSource2Test(TestCase):
self.assertIn(u"did not return a required parameter", result['error_html'])
@patch.object(OrderItem, 'purchased_callback')
- def test_sign_then_verify_unicode(self, purchased_callback):
+ @patch.object(OrderItem, 'pdf_receipt_display_name')
+ def test_sign_then_verify_unicode(self, pdf_receipt_display_name, purchased_callback): # pylint: disable=unused-argument
params = self._signed_callback_params(
self.order.id, self.COST, self.COST,
first_name=u'\u2699'
diff --git a/lms/djangoapps/shoppingcart/tests/test_pdf.py b/lms/djangoapps/shoppingcart/tests/test_pdf.py
new file mode 100644
index 0000000000..99c85e91f9
--- /dev/null
+++ b/lms/djangoapps/shoppingcart/tests/test_pdf.py
@@ -0,0 +1,235 @@
+"""
+Tests for Pdf file
+"""
+from datetime import datetime
+from django.test.utils import override_settings
+from django.conf import settings
+import unittest
+from io import BytesIO
+from shoppingcart.pdf import PDFInvoice
+from shoppingcart.utils import parse_pages
+
+PDF_RECEIPT_DISCLAIMER_TEXT = "THE SITE AND ANY INFORMATION, CONTENT OR SERVICES MADE AVAILABLE ON OR THROUGH " \
+ "THE SITE ARE PROVIDED \"AS IS\" AND \"AS AVAILABLE\" WITHOUT WARRANTY OF ANY KIND (EXPRESS, IMPLIED OR" \
+ " OTHERWISE), INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A " \
+ "PARTICULAR PURPOSE AND NON-INFRINGEMENT, EXCEPT INSOFAR AS ANY SUCH IMPLIED WARRANTIES MAY NOT BE DISCLAIMED" \
+ " UNDER APPLICABLE LAW."
+PDF_RECEIPT_BILLING_ADDRESS = "edX\n141 Portland St.\n9th Floor\nCambridge,\nMA 02139"
+PDF_RECEIPT_FOOTER_TEXT = "EdX offers online courses that include opportunities for professor-to-student and" \
+ " student-to-student interactivity, individual assessment of a student's work and, for students who demonstrate" \
+ " their mastery of subjects, a certificate of achievement or other acknowledgment."
+PDF_RECEIPT_TAX_ID = "46-0807740"
+PDF_RECEIPT_TAX_ID_LABEL = "edX Tax ID"
+PDF_RECEIPT_TERMS_AND_CONDITIONS = "Enrollments:\nEnrollments must be completed within 7 full days from the course " \
+ "start date.\nPayment Terms:\nPayment is due immediately. Preferred method of payment is wire transfer. Full " \
+ "instructions and remittance details will be included on your official invoice. Please note that our terms are " \
+ "net zero. For questions regarding payment instructions or extensions, please contact " \
+ "onlinex-registration@mit.edu and include the words \"payment question\" in your subject line.\nCancellations:" \
+ "\nCancellation requests must be submitted to onlinex-registration@mit.edu 14 days prior to the course start " \
+ "date to be eligible for a refund. If you submit a cancellation request within 14 days prior to the course start " \
+ "date, you will not be eligible for a refund. Please see our Terms of Service page for full details." \
+ "\nSubstitutions:\nThe MIT Professional Education Online X Programs office must receive substitution requests " \
+ "before the course start date in order for the request to be considered. Please email " \
+ "onlinex-registration@mit.edu to request a substitution.Please see our Terms of Service page for our detailed " \
+ "policies, including terms and conditions of use."
+
+
+class TestPdfFile(unittest.TestCase):
+ """
+ Unit test cases for pdf file generation
+ """
+ def setUp(self):
+ self.items_data = [self.get_item_data(1)]
+ self.item_id = '1'
+ self.date = datetime.now()
+ self.is_invoice = False
+ self.total_cost = 1000
+ self.payment_received = 1000
+ self.balance = 0
+ self.pdf_buffer = BytesIO()
+
+ def get_item_data(self, index, discount=0):
+ """
+ return the dictionary with the dummy data
+ """
+ return {
+ 'item_description': 'Course %s Description' % index,
+ 'quantity': index,
+ 'list_price': 10,
+ 'discount': discount,
+ 'item_total': 10
+ }
+
+ @override_settings(
+ PDF_RECEIPT_DISCLAIMER_TEXT=PDF_RECEIPT_DISCLAIMER_TEXT,
+ PDF_RECEIPT_BILLING_ADDRESS=PDF_RECEIPT_BILLING_ADDRESS,
+ PDF_RECEIPT_FOOTER_TEXT=PDF_RECEIPT_FOOTER_TEXT,
+ PDF_RECEIPT_TAX_ID=PDF_RECEIPT_TAX_ID,
+ PDF_RECEIPT_TAX_ID_LABEL=PDF_RECEIPT_TAX_ID_LABEL,
+ PDF_RECEIPT_TERMS_AND_CONDITIONS=PDF_RECEIPT_TERMS_AND_CONDITIONS,
+ )
+ def test_pdf_receipt_configured_generation(self):
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any('Receipt' in s for s in pdf_content))
+ self.assertTrue(any(str(self.total_cost) in s for s in pdf_content))
+ self.assertTrue(any(str(self.payment_received) in s for s in pdf_content))
+ self.assertTrue(any(str(self.balance) in s for s in pdf_content))
+ self.assertTrue(any('edX Tax ID' in s for s in pdf_content))
+
+ # PDF_RECEIPT_TERMS_AND_CONDITIONS not displayed in the receipt pdf
+ self.assertFalse(any(
+ 'Enrollments:\nEnrollments must be completed within 7 full days from the course'
+ ' start date.\nPayment Terms:\nPayment is due immediately.' in s for s in pdf_content
+ ))
+ self.assertTrue(any('edX\n141 Portland St.\n9th Floor\nCambridge,\nMA 02139' in s for s in pdf_content))
+
+ def test_pdf_receipt_not_configured_generation(self):
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any('Receipt' in s for s in pdf_content))
+ self.assertTrue(any(settings.PDF_RECEIPT_DISCLAIMER_TEXT in s for s in pdf_content))
+ self.assertTrue(any(settings.PDF_RECEIPT_BILLING_ADDRESS in s for s in pdf_content))
+ self.assertTrue(any(settings.PDF_RECEIPT_FOOTER_TEXT in s for s in pdf_content))
+ # PDF_RECEIPT_TERMS_AND_CONDITIONS not displayed in the receipt pdf
+ self.assertFalse(any(settings.PDF_RECEIPT_TERMS_AND_CONDITIONS in s for s in pdf_content))
+
+ @override_settings(
+ PDF_RECEIPT_DISCLAIMER_TEXT=PDF_RECEIPT_DISCLAIMER_TEXT,
+ PDF_RECEIPT_BILLING_ADDRESS=PDF_RECEIPT_BILLING_ADDRESS,
+ PDF_RECEIPT_FOOTER_TEXT=PDF_RECEIPT_FOOTER_TEXT,
+ PDF_RECEIPT_TAX_ID=PDF_RECEIPT_TAX_ID,
+ PDF_RECEIPT_TAX_ID_LABEL=PDF_RECEIPT_TAX_ID_LABEL,
+ PDF_RECEIPT_TERMS_AND_CONDITIONS=PDF_RECEIPT_TERMS_AND_CONDITIONS,
+ )
+ def test_pdf_receipt_file_item_data_pagination(self):
+ for i in range(2, 50):
+ self.items_data.append(self.get_item_data(i))
+
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any('Receipt' in s for s in pdf_content))
+ self.assertTrue(any('Page 3 of 3' in s for s in pdf_content))
+
+ def test_pdf_receipt_file_totals_pagination(self):
+ for i in range(2, 48):
+ self.items_data.append(self.get_item_data(i))
+
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any('Receipt' in s for s in pdf_content))
+ self.assertTrue(any('Page 3 of 3' in s for s in pdf_content))
+
+ @override_settings(PDF_RECEIPT_LOGO_PATH='wrong path')
+ def test_invalid_image_path(self):
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any('Receipt' in s for s in pdf_content))
+
+ def test_pdf_receipt_file_footer_pagination(self):
+ for i in range(2, 44):
+ self.items_data.append(self.get_item_data(i))
+
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any('Receipt' in s for s in pdf_content))
+
+ @override_settings(
+ PDF_RECEIPT_DISCLAIMER_TEXT=PDF_RECEIPT_DISCLAIMER_TEXT,
+ PDF_RECEIPT_BILLING_ADDRESS=PDF_RECEIPT_BILLING_ADDRESS,
+ PDF_RECEIPT_FOOTER_TEXT=PDF_RECEIPT_FOOTER_TEXT,
+ PDF_RECEIPT_TAX_ID=PDF_RECEIPT_TAX_ID,
+ PDF_RECEIPT_TAX_ID_LABEL=PDF_RECEIPT_TAX_ID_LABEL,
+ PDF_RECEIPT_TERMS_AND_CONDITIONS=PDF_RECEIPT_TERMS_AND_CONDITIONS,
+ )
+ def test_pdf_invoice_with_settings_from_patch(self):
+ self.is_invoice = True
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any('46-0807740' in s for s in pdf_content))
+ self.assertTrue(any('Invoice' in s for s in pdf_content))
+ self.assertTrue(any(str(self.total_cost) in s for s in pdf_content))
+ self.assertTrue(any(str(self.payment_received) in s for s in pdf_content))
+ self.assertTrue(any(str(self.balance) in s for s in pdf_content))
+ self.assertTrue(any('edX Tax ID' in s for s in pdf_content))
+ self.assertTrue(any(
+ 'Enrollments:\nEnrollments must be completed within 7 full'
+ ' days from the course start date.\nPayment Terms:\nPayment'
+ ' is due immediately.' in s for s in pdf_content))
+
+ def test_pdf_invoice_with_default_settings(self):
+ self.is_invoice = True
+ PDFInvoice(
+ items_data=self.items_data,
+ item_id=self.item_id,
+ date=self.date,
+ is_invoice=self.is_invoice,
+ total_cost=self.total_cost,
+ payment_received=self.payment_received,
+ balance=self.balance
+ ).generate_pdf(self.pdf_buffer)
+
+ pdf_content = parse_pages(self.pdf_buffer, 'test_pass')
+ self.assertTrue(any(settings.PDF_RECEIPT_TAX_ID in s for s in pdf_content))
+ self.assertTrue(any('Invoice' in s for s in pdf_content))
+ self.assertTrue(any(settings.PDF_RECEIPT_TERMS_AND_CONDITIONS in s for s in pdf_content))
diff --git a/lms/djangoapps/shoppingcart/utils.py b/lms/djangoapps/shoppingcart/utils.py
index de469f29be..9c193066b8 100644
--- a/lms/djangoapps/shoppingcart/utils.py
+++ b/lms/djangoapps/shoppingcart/utils.py
@@ -4,6 +4,12 @@ Utility methods for the Shopping Cart app
from django.conf import settings
from microsite_configuration import microsite
+from pdfminer.pdfparser import PDFParser
+from pdfminer.pdfdocument import PDFDocument
+from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
+from pdfminer.converter import PDFPageAggregator
+from pdfminer.pdfpage import PDFPage
+from pdfminer.layout import LAParams, LTTextBox, LTTextLine, LTFigure
def is_shopping_cart_enabled():
@@ -22,3 +28,47 @@ def is_shopping_cart_enabled():
)
return (enable_paid_course_registration and enable_shopping_cart)
+
+
+def parse_pages(pdf_buffer, password):
+ """
+ With an PDF buffer object, get the pages, parse each one, and return the entire pdf text
+ """
+ # Create a PDF parser object associated with the file object.
+ parser = PDFParser(pdf_buffer)
+ # Create a PDF document object that stores the document structure.
+ # Supply the password for initialization.
+ document = PDFDocument(parser, password)
+
+ resource_manager = PDFResourceManager()
+ la_params = LAParams()
+ device = PDFPageAggregator(resource_manager, laparams=la_params)
+ interpreter = PDFPageInterpreter(resource_manager, device)
+
+ text_content = [] # a list of strings, each representing text collected from each page of the doc
+ for page in PDFPage.create_pages(document):
+ interpreter.process_page(page)
+ # receive the LTPage object for this page
+ layout = device.get_result()
+ # layout is an LTPage object which may contain
+ # child objects like LTTextBox, LTFigure, LTImage, etc.
+ text_content.append(parse_lt_objects(layout._objs)) # pylint: disable=protected-access
+
+ return text_content
+
+
+def parse_lt_objects(lt_objects):
+ """
+ Iterate through the list of LT* objects and capture the text data contained in each object
+ """
+ text_content = []
+
+ for lt_object in lt_objects:
+ if isinstance(lt_object, LTTextBox) or isinstance(lt_object, LTTextLine):
+ # text
+ text_content.append(lt_object.get_text().encode('utf-8'))
+ elif isinstance(lt_object, LTFigure):
+ # LTFigure objects are containers for other LT* objects, so recurse through the children
+ text_content.append(parse_lt_objects(lt_object._objs)) # pylint: disable=protected-access
+
+ return '\n'.join(text_content)
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index b21b0e9ebe..70257e889c 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -478,3 +478,17 @@ INVOICE_PAYMENT_INSTRUCTIONS = ENV_TOKENS.get('INVOICE_PAYMENT_INSTRUCTIONS', IN
#date format the api will be formatting the datetime values
API_DATE_FORMAT = '%Y-%m-%d'
API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT)
+
+# PDF RECEIPT/INVOICE OVERRIDES
+PDF_RECEIPT_TAX_ID = ENV_TOKENS.get('PDF_RECEIPT_TAX_ID', PDF_RECEIPT_TAX_ID)
+PDF_RECEIPT_FOOTER_TEXT = ENV_TOKENS.get('PDF_RECEIPT_FOOTER_TEXT', PDF_RECEIPT_FOOTER_TEXT)
+PDF_RECEIPT_DISCLAIMER_TEXT = ENV_TOKENS.get('PDF_RECEIPT_DISCLAIMER_TEXT', PDF_RECEIPT_DISCLAIMER_TEXT)
+PDF_RECEIPT_BILLING_ADDRESS = ENV_TOKENS.get('PDF_RECEIPT_BILLING_ADDRESS', PDF_RECEIPT_BILLING_ADDRESS)
+PDF_RECEIPT_TERMS_AND_CONDITIONS = ENV_TOKENS.get('PDF_RECEIPT_TERMS_AND_CONDITIONS', PDF_RECEIPT_TERMS_AND_CONDITIONS)
+PDF_RECEIPT_TAX_ID_LABEL = ENV_TOKENS.get('PDF_RECEIPT_TAX_ID_LABEL', PDF_RECEIPT_TAX_ID_LABEL)
+PDF_RECEIPT_LOGO_PATH = ENV_TOKENS.get('PDF_RECEIPT_LOGO_PATH', PDF_RECEIPT_LOGO_PATH)
+PDF_RECEIPT_COBRAND_LOGO_PATH = ENV_TOKENS.get('PDF_RECEIPT_COBRAND_LOGO_PATH', PDF_RECEIPT_COBRAND_LOGO_PATH)
+PDF_RECEIPT_LOGO_HEIGHT_MM = ENV_TOKENS.get('PDF_RECEIPT_LOGO_HEIGHT_MM', PDF_RECEIPT_LOGO_HEIGHT_MM)
+PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM = ENV_TOKENS.get(
+ 'PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM', PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM
+)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 95bdaa01e4..179a9a82e0 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1970,3 +1970,17 @@ API_DATE_FORMAT = '%Y-%m-%d'
# for Student Notes we would like to avoid too frequent token refreshes (default is 30 seconds)
if FEATURES['ENABLE_EDXNOTES']:
OAUTH_ID_TOKEN_EXPIRATION = 60 * 60
+
+# Configuration used for generating PDF Receipts/Invoices
+PDF_RECEIPT_TAX_ID = 'add here'
+PDF_RECEIPT_FOOTER_TEXT = 'add your own specific footer text here'
+PDF_RECEIPT_DISCLAIMER_TEXT = 'add your own specific disclaimer text here'
+PDF_RECEIPT_BILLING_ADDRESS = 'add your own billing address here with appropriate line feed characters'
+PDF_RECEIPT_TERMS_AND_CONDITIONS = 'add your own terms and conditions'
+PDF_RECEIPT_TAX_ID_LABEL = 'Tax ID'
+PDF_RECEIPT_LOGO_PATH = PROJECT_ROOT + '/static/images/openedx-logo-tag.png'
+# Height of the Logo in mm
+PDF_RECEIPT_LOGO_HEIGHT_MM = 12
+PDF_RECEIPT_COBRAND_LOGO_PATH = PROJECT_ROOT + '/static/images/default-theme/logo.png'
+# Height of the Co-brand Logo in mm
+PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM = 12
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 56140cb670..770f09597b 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -88,6 +88,12 @@ django-ratelimit-backend==0.6
unicodecsv==0.9.4
django-require==1.0.6
+# Used for shopping cart's pdf invoice/receipt generation
+reportlab==3.1.44
+
+# Used for extracting/parsing pdf text
+pdfminer==20140328
+
# Used for development operation
watchdog==0.7.1