Remove shoppingcart pdf generation.
DEPR-40
This commit is contained in:
@@ -79,7 +79,6 @@ from shoppingcart.models import (
|
||||
PaidCourseRegistration,
|
||||
RegistrationCodeRedemption
|
||||
)
|
||||
from shoppingcart.pdf import PDFInvoice
|
||||
from student.models import (
|
||||
ALLOWEDTOENROLL_TO_ENROLLED,
|
||||
ALLOWEDTOENROLL_TO_UNENROLLED,
|
||||
@@ -5131,25 +5130,6 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase):
|
||||
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': text_type(self.course.id)}
|
||||
)
|
||||
data = {
|
||||
'total_registration_codes': 9, 'company_name': 'Group Alpha', 'company_contact_name': 'Test@company.com',
|
||||
'company_contact_email': 'Test@company.com', 'unit_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
|
||||
|
||||
@@ -1809,12 +1809,6 @@ 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 = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
|
||||
context = {
|
||||
'invoice': sale_invoice,
|
||||
@@ -1867,11 +1861,6 @@ 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(_('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)
|
||||
|
||||
@@ -10,7 +10,6 @@ import smtplib
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
|
||||
import pytz
|
||||
import six
|
||||
@@ -39,7 +38,6 @@ from courseware.courses import get_course_by_id
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from shoppingcart.pdf import PDFInvoice
|
||||
from student.models import CourseEnrollment, EnrollStatusChange
|
||||
from student.signals import UNENROLL_DONE
|
||||
from track import segment
|
||||
@@ -307,34 +305,6 @@ 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:
|
||||
item_total = item.qty * item.unit_cost
|
||||
items_data.append({
|
||||
'item_description': item.pdf_receipt_display_name,
|
||||
'quantity': item.qty,
|
||||
'list_price': item.get_list_price(),
|
||||
'discount': item.get_list_price() - item.unit_cost,
|
||||
'item_total': item_total
|
||||
})
|
||||
pdf_buffer = BytesIO()
|
||||
|
||||
PDFInvoice(
|
||||
items_data=items_data,
|
||||
item_id=str(self.id),
|
||||
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
|
||||
@@ -355,7 +325,7 @@ class Order(models.Model):
|
||||
|
||||
return csv_file, course_names
|
||||
|
||||
def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, pdf_file, site_name, course_names):
|
||||
def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, site_name, course_names):
|
||||
"""
|
||||
send confirmation e-mail
|
||||
"""
|
||||
@@ -420,11 +390,6 @@ 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'ReceiptOrder{}.pdf'.format(str(self.id)), pdf_file.getvalue(), 'application/pdf')
|
||||
else:
|
||||
file_buffer = six.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(u'Failed sending confirmation e-mail for order %d', self.id)
|
||||
@@ -491,16 +456,10 @@ class Order(models.Model):
|
||||
#
|
||||
csv_file, course_names = self.generate_registration_codes_csv(orderitems, site_name)
|
||||
|
||||
try:
|
||||
pdf_file = self.generate_pdf_receipt(orderitems)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception('Exception at creating pdf file.')
|
||||
pdf_file = None
|
||||
|
||||
try:
|
||||
self.send_confirmation_emails(
|
||||
orderitems, self.order_type == OrderTypes.BUSINESS,
|
||||
csv_file, pdf_file, site_name, course_names
|
||||
csv_file, site_name, course_names
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Catch all exceptions here, since the Django view implicitly
|
||||
@@ -891,33 +850,6 @@ class Invoice(TimeStampedModel):
|
||||
total = result.get('total', 0)
|
||||
return total if total else 0
|
||||
|
||||
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),
|
||||
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
|
||||
|
||||
def snapshot(self):
|
||||
"""Create a snapshot of the invoice.
|
||||
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
"""
|
||||
Template for PDF Receipt/Invoice Generation
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from PIL import Image
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
from reportlab.platypus.tables import Table, TableStyle
|
||||
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from xmodule.modulestore.django import ModuleI18nService
|
||||
|
||||
log = logging.getLogger("PDF Generation")
|
||||
|
||||
|
||||
class NumberedCanvas(Canvas):
|
||||
"""
|
||||
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,
|
||||
_(u"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 = configuration_helpers.get_value("PDF_RECEIPT_LOGO_PATH", settings.PDF_RECEIPT_LOGO_PATH)
|
||||
self.cobrand_logo_path = configuration_helpers.get_value(
|
||||
"PDF_RECEIPT_COBRAND_LOGO_PATH", settings.PDF_RECEIPT_COBRAND_LOGO_PATH
|
||||
)
|
||||
self.tax_label = configuration_helpers.get_value("PDF_RECEIPT_TAX_ID_LABEL", settings.PDF_RECEIPT_TAX_ID_LABEL)
|
||||
self.tax_id = configuration_helpers.get_value("PDF_RECEIPT_TAX_ID", settings.PDF_RECEIPT_TAX_ID)
|
||||
self.footer_text = configuration_helpers.get_value("PDF_RECEIPT_FOOTER_TEXT", settings.PDF_RECEIPT_FOOTER_TEXT)
|
||||
self.disclaimer_text = configuration_helpers.get_value(
|
||||
"PDF_RECEIPT_DISCLAIMER_TEXT", settings.PDF_RECEIPT_DISCLAIMER_TEXT,
|
||||
)
|
||||
self.billing_address_text = configuration_helpers.get_value(
|
||||
"PDF_RECEIPT_BILLING_ADDRESS", settings.PDF_RECEIPT_BILLING_ADDRESS
|
||||
)
|
||||
self.terms_conditions_text = configuration_helpers.get_value(
|
||||
"PDF_RECEIPT_TERMS_AND_CONDITIONS", settings.PDF_RECEIPT_TERMS_AND_CONDITIONS
|
||||
)
|
||||
self.brand_logo_height = configuration_helpers.get_value(
|
||||
"PDF_RECEIPT_LOGO_HEIGHT_MM", settings.PDF_RECEIPT_LOGO_HEIGHT_MM
|
||||
) * mm
|
||||
self.cobrand_logo_height = configuration_helpers.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 as ex:
|
||||
log.exception(u'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
|
||||
if self.cobrand_logo_path:
|
||||
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
|
||||
if self.logo_path:
|
||||
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]
|
||||
]
|
||||
|
||||
if self.is_invoice:
|
||||
# only print TaxID if we are generating an Invoice
|
||||
totals_data.append(
|
||||
['', u'{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)
|
||||
|
||||
styles = [
|
||||
# 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
|
||||
# NOTE: since we are not printing the TaxID for Credit Card
|
||||
# based receipts, we need to change the cell range for
|
||||
# these formatting rules
|
||||
('RIGHTPADDING', (-1, 0), (-1, 2), 7 * mm),
|
||||
('GRID', (-1, 0), (-1, 2), 3.0, colors.white),
|
||||
('BACKGROUND', (-1, 0), (-1, 2), '#EEEEEE'),
|
||||
]
|
||||
|
||||
totals_table.setStyle(TableStyle(styles))
|
||||
|
||||
__, 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", "<br/>"), para_style)
|
||||
disclaimer_para = Paragraph(self.disclaimer_text.replace("\n", "<br/>"), para_style)
|
||||
billing_address_para = Paragraph(self.billing_address_text.replace("\n", "<br/>"), 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", "<br/>"), 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)
|
||||
@@ -1,243 +0,0 @@
|
||||
"""
|
||||
Tests for Pdf file
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from six.moves import range
|
||||
|
||||
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):
|
||||
super(TestPdfFile, self).setUp()
|
||||
|
||||
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': u'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.assertFalse(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))
|
||||
@@ -129,7 +129,6 @@ python3-openid ; python_version>='3'
|
||||
python3-saml
|
||||
pyuca==1.1 # For more accurate sorting of translated country names in django-countries
|
||||
recommender-xblock # https://github.com/edx/RecommenderXBlock
|
||||
reportlab # Used for shopping cart's pdf invoice/receipt generation
|
||||
rest-condition # DRF's recommendation for supporting complex permissions
|
||||
rfc6266-parser # Used to generate Content-Disposition headers.
|
||||
social-auth-app-django<3.0.0
|
||||
|
||||
@@ -209,7 +209,6 @@ pyuca==1.1
|
||||
pyyaml==5.1.2
|
||||
recommender-xblock==1.4.4
|
||||
redis==2.10.6
|
||||
reportlab==3.5.26
|
||||
requests-oauthlib==1.1.0
|
||||
requests==2.22.0
|
||||
rest-condition==1.0.3
|
||||
|
||||
@@ -282,7 +282,6 @@ radon==4.0.0
|
||||
recommender-xblock==1.4.4
|
||||
recommonmark==0.6.0
|
||||
redis==2.10.6
|
||||
reportlab==3.5.26
|
||||
requests-oauthlib==1.1.0
|
||||
requests==2.22.0
|
||||
rest-condition==1.0.3
|
||||
|
||||
@@ -272,7 +272,6 @@ pyyaml==5.1.2
|
||||
radon==4.0.0
|
||||
recommender-xblock==1.4.4
|
||||
redis==2.10.6
|
||||
reportlab==3.5.26
|
||||
requests-oauthlib==1.1.0
|
||||
requests==2.22.0
|
||||
rest-condition==1.0.3
|
||||
|
||||
Reference in New Issue
Block a user