From 04f0bbd2ba350c2456f72af900bbb21cfbfa4e68 Mon Sep 17 00:00:00 2001 From: Andrew Colin Kissa Date: Tue, 20 Feb 2018 12:23:55 +0200 Subject: [PATCH] FET: Add po box and private bag attributes Support Post office box and private bag attributes for addresses --- pyinvoice/models.py | 42 ++++++++--- pyinvoice/templates.py | 159 +++++++++++++++++++++++++++++----------- tests/test_templates.py | 86 ++++++++++++++++++++++ 3 files changed, 236 insertions(+), 51 deletions(-) diff --git a/pyinvoice/models.py b/pyinvoice/models.py index 7a4d171..a1fba7a 100644 --- a/pyinvoice/models.py +++ b/pyinvoice/models.py @@ -26,7 +26,8 @@ class InvoiceInfo(object): """ Invoice information """ - def __init__(self, invoice_id=None, invoice_datetime=None, due_datetime=None): + def __init__( + self, invoice_id=None, invoice_datetime=None, due_datetime=None): """ Invoice info :param invoice_id: Invoice id @@ -42,10 +43,16 @@ def __init__(self, invoice_id=None, invoice_datetime=None, due_datetime=None): class AddressInfo(object): - def __init__(self, name=None, street=None, city=None, state=None, country=None, post_code=None): + def __init__( + self, name=None, street=None, + po_box=None, private_bag=None, + city=None, state=None, + country=None, post_code=None): """ :type name: str or unicode or None :type street: str or unicode or None + :type po_box: str or unicode or None + :type private_bag: str or unicode or None :type city: str or unicode or None :type state: str or unicode or None :type country: str or unicode or None @@ -53,6 +60,8 @@ def __init__(self, name=None, street=None, city=None, state=None, country=None, """ self.name = name self.street = street + self.po_box = po_box + self.private_bag = private_bag self.city = city self.state = state self.country = country @@ -63,18 +72,26 @@ class ServiceProviderInfo(AddressInfo): """ Service provider/Merchant information """ - def __init__(self, name=None, street=None, city=None, state=None, country=None, post_code=None, - vat_tax_number=None): + def __init__( + self, name=None, street=None, + po_box=None, private_bag=None, + city=None, state=None, country=None, + post_code=None, + vat_tax_number=None): """ :type name: str or unicode or None :type street: str or unicode or None + :type po_box: str or unicode or None + :type private_bag: str or unicode or None :type city: str or unicode or None :type state: str or unicode or None :type country: str or unicode or None :type post_code: str or unicode or None :type vat_tax_number: str or unicode or int or None """ - super(ServiceProviderInfo, self).__init__(name, street, city, state, country, post_code) + super(ServiceProviderInfo, self).__init__( + name, street, po_box, private_bag, city, + state, country, post_code) self.vat_tax_number = vat_tax_number @@ -82,11 +99,16 @@ class ClientInfo(AddressInfo): """ Client/Custom information """ - def __init__(self, name=None, street=None, city=None, state=None, country=None, post_code=None, - email=None, client_id=None): + def __init__( + self, name=None, street=None, + po_box=None, private_bag=None, + city=None, state=None, country=None, + post_code=None, email=None, client_id=None): """ :type name: str or unicode or None :type street: str or unicode or None + :type po_box: str or unicode or None + :type private_bag: str or unicode or None :type city: str or unicode or None :type state: str or unicode or None :type country: str or unicode or None @@ -94,7 +116,9 @@ def __init__(self, name=None, street=None, city=None, state=None, country=None, :type email: str or unicode or None :type client_id: str or unicode or int or None """ - super(ClientInfo, self).__init__(name, street, city, state, country, post_code) + super(ClientInfo, self).__init__( + name, street, po_box, private_bag, + city, state, country, post_code) self.email = email self.client_id = client_id @@ -145,4 +169,4 @@ def __init__(self, gateway, transaction_id, transaction_datetime, amount): self.gateway = gateway self.transaction_id = transaction_id self.transaction_datetime = transaction_datetime - self.amount = amount \ No newline at end of file + self.amount = amount diff --git a/pyinvoice/templates.py b/pyinvoice/templates.py index f966378..699e48f 100644 --- a/pyinvoice/templates.py +++ b/pyinvoice/templates.py @@ -10,11 +10,13 @@ from reportlab.platypus import SimpleDocTemplate, Paragraph, Table, Spacer from pyinvoice.components import SimpleTable, TableWithHeader, PaidStamp -from pyinvoice.models import PDFInfo, Item, Transaction, InvoiceInfo, ServiceProviderInfo, ClientInfo +from pyinvoice.models import PDFInfo, Item, Transaction, InvoiceInfo, \ + ServiceProviderInfo, ClientInfo class SimpleInvoice(SimpleDocTemplate): - default_pdf_info = PDFInfo(title='Invoice', author='CiCiApp.com', subject='Invoice') + default_pdf_info = PDFInfo( + title='Invoice', author='CiCiApp.com', subject='Invoice') precision = None def __init__(self, invoice_path, pdf_info=None, precision='0.01'): @@ -36,10 +38,14 @@ def __init__(self, invoice_path, pdf_info=None, precision='0.01'): self._defined_styles = getSampleStyleSheet() self._defined_styles.add( - ParagraphStyle('RightHeading1', parent=self._defined_styles.get('Heading1'), alignment=TA_RIGHT) + ParagraphStyle( + 'RightHeading1', parent=self._defined_styles.get('Heading1'), + alignment=TA_RIGHT) ) self._defined_styles.add( - ParagraphStyle('TableParagraph', parent=self._defined_styles.get('Normal'), alignment=TA_CENTER) + ParagraphStyle( + 'TableParagraph', parent=self._defined_styles.get('Normal'), + alignment=TA_CENTER) ) self.invoice_info = None @@ -97,8 +103,10 @@ def _attribute_to_table_data(self, instance, attribute_verbose_name_list): def _invoice_info_data(self): if isinstance(self.invoice_info, InvoiceInfo): - props = [('invoice_id', 'Invoice id'), ('invoice_datetime', 'Invoice date'), - ('due_datetime', 'Invoice due date')] + props = [ + ('invoice_id', 'Invoice id'), + ('invoice_datetime', 'Invoice date'), + ('due_datetime', 'Invoice due date')] return self._attribute_to_table_data(self.invoice_info, props) @@ -107,15 +115,29 @@ def _invoice_info_data(self): def _build_invoice_info(self): invoice_info_data = self._invoice_info_data() if invoice_info_data: - self._story.append(Paragraph('Invoice', self._defined_styles.get('RightHeading1'))) - self._story.append(SimpleTable(invoice_info_data, horizontal_align='RIGHT')) + self._story.append( + Paragraph( + 'Invoice', + self._defined_styles.get('RightHeading1')) + ) + self._story.append( + SimpleTable(invoice_info_data, horizontal_align='RIGHT')) def _service_provider_data(self): if isinstance(self.service_provider_info, ServiceProviderInfo): - props = [('name', 'Name'), ('street', 'Street'), ('city', 'City'), ('state', 'State'), - ('country', 'Country'), ('post_code', 'Post code'), ('vat_tax_number', 'Vat/Tax number')] - - return self._attribute_to_table_data(self.service_provider_info, props) + props = [ + ('name', 'Name'), + ('street', 'Street'), + ('po_box', 'P.O Box'), + ('private_bag', 'Private Bag'), + ('city', 'City'), + ('state', 'State'), + ('country', 'Country'), + ('post_code', 'Post code'), + ('vat_tax_number', 'Vat/Tax number')] + + return self._attribute_to_table_data( + self.service_provider_info, props) return [] @@ -123,32 +145,53 @@ def _build_service_provider_info(self): # Merchant service_provider_info_data = self._service_provider_data() if service_provider_info_data: - self._story.append(Paragraph('Service Provider', self._defined_styles.get('RightHeading1'))) - self._story.append(SimpleTable(service_provider_info_data, horizontal_align='RIGHT')) + self._story.append( + Paragraph( + 'Service Provider', + self._defined_styles.get('RightHeading1')) + ) + self._story.append( + SimpleTable( + service_provider_info_data, horizontal_align='RIGHT')) def _client_info_data(self): if not isinstance(self.client_info, ClientInfo): return [] - props = [('name', 'Name'), ('street', 'Street'), ('city', 'City'), ('state', 'State'), - ('country', 'Country'), ('post_code', 'Post code'), ('email', 'Email'), ('client_id', 'Client id')] + props = [ + ('name', 'Name'), + ('street', 'Street'), + ('po_box', 'P.O Box'), + ('private_bag', 'Private Bag'), + ('city', 'City'), + ('state', 'State'), + ('country', 'Country'), + ('post_code', 'Post code'), + ('email', 'Email'), + ('client_id', 'Client id') + ] return self._attribute_to_table_data(self.client_info, props) def _build_client_info(self): # ClientInfo client_info_data = self._client_info_data() if client_info_data: - self._story.append(Paragraph('Client', self._defined_styles.get('Heading1'))) - self._story.append(SimpleTable(client_info_data, horizontal_align='LEFT')) + self._story.append( + Paragraph('Client', self._defined_styles.get('Heading1'))) + self._story.append( + SimpleTable(client_info_data, horizontal_align='LEFT')) def _build_service_provider_and_client_info(self): - if isinstance(self.service_provider_info, ServiceProviderInfo) and isinstance(self.client_info, ClientInfo): + if isinstance(self.service_provider_info, ServiceProviderInfo) and \ + isinstance(self.client_info, ClientInfo): # Merge Table table_data = [ [ - Paragraph('Service Provider', self._defined_styles.get('Heading1')), '', - '', - Paragraph('Client', self._defined_styles.get('Heading1')), '' + Paragraph( + 'Service Provider', + self._defined_styles.get('Heading1')), '', '', + Paragraph('Client', self._defined_styles.get('Heading1')), + '' ] ] table_style = [ @@ -163,9 +206,9 @@ def _build_service_provider_and_client_info(self): diff = abs(len(client_info_data) - len(service_provider_data)) if diff > 0: if len(client_info_data) < len(service_provider_data): - client_info_data.extend([["", ""]]*diff) + client_info_data.extend([["", ""]] * diff) else: - service_provider_data.extend([["", ""]*diff]) + service_provider_data.extend([["", ""] * diff]) for d in zip(service_provider_data, client_info_data): d[0].append('') d[0].extend(d[1]) @@ -188,7 +231,9 @@ def _item_raw_data_and_subtotal(self): item_data.append( ( item.name, - Paragraph(item.description, self._defined_styles.get('TableParagraph')), + Paragraph( + item.description, + self._defined_styles.get('TableParagraph')), item.units, item.unit_price, item.amount @@ -210,7 +255,8 @@ def _item_data_and_style(self): Paragraph('Detail', self._defined_styles.get('Heading1')) ) - item_data_title = ('Name', 'Description', 'Units', 'Unit Price', 'Amount') + item_data_title = ( + 'Name', 'Description', 'Units', 'Unit Price', 'Amount') item_data.insert(0, item_data_title) # Insert title # Summary field @@ -219,24 +265,41 @@ def _item_data_and_style(self): sum_start_x_index = len(item_data_title) - abs(sum_end_x_index) # ##### Subtotal ##### - rounditem_subtotal = self.getroundeddecimal(item_subtotal, self.precision) + rounditem_subtotal = self.getroundeddecimal( + item_subtotal, self.precision) item_data.append( ('Subtotal', '', '', '', rounditem_subtotal) ) - style.append(('SPAN', (0, sum_start_y_index), (sum_start_x_index, sum_start_y_index))) - style.append(('ALIGN', (0, sum_start_y_index), (sum_end_x_index, -1), 'RIGHT')) + style.append(( + 'SPAN', + (0, sum_start_y_index), + (sum_start_x_index, sum_start_y_index))) + style.append(( + 'ALIGN', + (0, sum_start_y_index), + (sum_end_x_index, -1), + 'RIGHT')) # Tax total if self._item_tax_rate is not None: - tax_total = item_subtotal * (Decimal(str(self._item_tax_rate)) / Decimal('100')) + tax_total = item_subtotal * \ + (Decimal(str(self._item_tax_rate)) / Decimal('100')) roundtax_total = self.getroundeddecimal(tax_total, self.precision) item_data.append( - ('Vat/Tax ({0}%)'.format(self._item_tax_rate), '', '', '', roundtax_total) + ('Vat/Tax ({0}%)'.format(self._item_tax_rate), + '', '', '', roundtax_total) ) sum_start_y_index += 1 - style.append(('SPAN', (0, sum_start_y_index), (sum_start_x_index, sum_start_y_index))) - style.append(('ALIGN', (0, sum_start_y_index), (sum_end_x_index, -1), 'RIGHT')) + style.append(( + 'SPAN', + (0, sum_start_y_index), + (sum_start_x_index, sum_start_y_index))) + style.append(( + 'ALIGN', + (0, sum_start_y_index), + (sum_end_x_index, -1), + 'RIGHT')) else: tax_total = None @@ -245,34 +308,44 @@ def _item_data_and_style(self): roundtotal = self.getroundeddecimal(total, self.precision) item_data.append(('Total', '', '', '', roundtotal)) sum_start_y_index += 1 - style.append(('SPAN', (0, sum_start_y_index), (sum_start_x_index, sum_start_y_index))) - style.append(('ALIGN', (0, sum_start_y_index), (sum_end_x_index, -1), 'RIGHT')) + style.append(( + 'SPAN', + (0, sum_start_y_index), + (sum_start_x_index, sum_start_y_index))) + style.append(( + 'ALIGN', + (0, sum_start_y_index), + (sum_end_x_index, -1), + 'RIGHT')) return item_data, style def getroundeddecimal(self, nrtoround, precision): d = Decimal(nrtoround) - aftercomma = Decimal(precision) # or anything that has the exponent depth you want + aftercomma = Decimal(precision) rvalue = Decimal(d.quantize(aftercomma, rounding='ROUND_HALF_UP')) return rvalue def _build_items(self): item_data, style = self._item_data_and_style() if item_data: - self._story.append(TableWithHeader(item_data, horizontal_align='LEFT', style=style)) + self._story.append(TableWithHeader( + item_data, horizontal_align='LEFT', style=style)) def _transactions_data(self): transaction_table_data = [ ( t.transaction_id, - Paragraph(t.gateway, self._defined_styles.get('TableParagraph')), + Paragraph( + t.gateway, self._defined_styles.get('TableParagraph')), self._format_value(t.transaction_datetime), t.amount, ) for t in self._transactions if isinstance(t, Transaction) ] if transaction_table_data: - transaction_table_data.insert(0, ('Transaction id', 'Gateway', 'Transaction date', 'Amount')) + transaction_table_data.insert( + 0, ('Transaction id', 'Gateway', 'Transaction date', 'Amount')) return transaction_table_data @@ -281,8 +354,10 @@ def _build_transactions(self): transaction_table_data = self._transactions_data() if transaction_table_data: - self._story.append(Paragraph('Transaction', self._defined_styles.get('Heading1'))) - self._story.append(TableWithHeader(transaction_table_data, horizontal_align='LEFT')) + self._story.append( + Paragraph('Transaction', self._defined_styles.get('Heading1'))) + self._story.append(TableWithHeader( + transaction_table_data, horizontal_align='LEFT')) def _build_bottom_tip(self): if self._bottom_tip: @@ -311,4 +386,4 @@ def finish(self): if self.is_paid: kwargs['onFirstPage'] = PaidStamp(7 * inch, 5.8 * inch) - self.build(self._story, **kwargs) \ No newline at end of file + self.build(self._story, **kwargs) diff --git a/tests/test_templates.py b/tests/test_templates.py index 2e69eb9..032652a 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -180,6 +180,92 @@ def test_invoice_info(self): self.assertTrue(os.path.exists(invoice_path)) + def test_service_provider_info_pobox(self): + invoice_path = os.path.join( + self.file_base_dir, 'service_provider_info_pobox.pdf') + if os.path.exists(invoice_path): + os.remove(invoice_path) + + invoice = SimpleInvoice(invoice_path) + + # Before add service provider info + info_data = invoice._service_provider_data() + self.assertEqual(info_data, []) + + # Empty info + invoice.service_provider_info = ServiceProviderInfo() + info_data = invoice._service_provider_data() + self.assertEqual(info_data, []) + + invoice.service_provider_info = ServiceProviderInfo( + name='CiCiApp', + po_box='1037', + city='City ccc', + state='State sss', + country='Country rrr', + post_code='Post code ppp', + vat_tax_number=666 + ) + + # After add service provider info + info_data = invoice._service_provider_data() + self.assertEqual(len(info_data), 7) + self.assertEqual(info_data[0][0], 'Name:') + self.assertEqual(info_data[0][1], 'CiCiApp') + self.assertEqual(info_data[1][0], 'P.O Box:') + self.assertEqual(info_data[1][1], '1037') + self.assertEqual(info_data[4][0], 'Country:') + self.assertEqual(info_data[4][1], 'Country rrr') + self.assertEqual(info_data[6][0], 'Vat/Tax number:') + self.assertEqual(info_data[6][1], 666) + + invoice.finish() + + self.assertTrue(os.path.exists(invoice_path)) + + def test_service_provider_info_pbag(self): + invoice_path = os.path.join( + self.file_base_dir, 'service_provider_info_pbag.pdf') + if os.path.exists(invoice_path): + os.remove(invoice_path) + + invoice = SimpleInvoice(invoice_path) + + # Before add service provider info + info_data = invoice._service_provider_data() + self.assertEqual(info_data, []) + + # Empty info + invoice.service_provider_info = ServiceProviderInfo() + info_data = invoice._service_provider_data() + self.assertEqual(info_data, []) + + invoice.service_provider_info = ServiceProviderInfo( + name='CiCiApp', + private_bag='1037', + city='City ccc', + state='State sss', + country='Country rrr', + post_code='Post code ppp', + vat_tax_number=666 + ) + + # After add service provider info + info_data = invoice._service_provider_data() + self.assertEqual(len(info_data), 7) + self.assertEqual(info_data[0][0], 'Name:') + self.assertEqual(info_data[0][1], 'CiCiApp') + self.assertEqual(info_data[1][0], 'Private Bag:') + self.assertEqual(info_data[1][1], '1037') + self.assertEqual(info_data[4][0], 'Country:') + self.assertEqual(info_data[4][1], 'Country rrr') + self.assertEqual(info_data[6][0], 'Vat/Tax number:') + self.assertEqual(info_data[6][1], 666) + + invoice.finish() + + self.assertTrue(os.path.exists(invoice_path)) + def test_service_provider_info(self): invoice_path = os.path.join(self.file_base_dir, 'service_provider_info.pdf') if os.path.exists(invoice_path):