diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f463de6..0000000 --- a/.gitignore +++ /dev/null @@ -1,62 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - - -.idea -tests/fixtures/dist/*.pdf -.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 14b80ab..0000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: python - -python: - - "2.6" - - "2.7" - - "3.3" - - "3.4" - -env: - - REPORTLAB=2.6 - - REPORTLAB=2.7 - - REPORTLAB=3.0 - - REPORTLAB=3.1.44 - - REPORTLAB=3.2 - -matrix: - exclude: - - env: REPORTLAB=2.6 - python: "3.3" - - env: REPORTLAB=2.6 - python: "3.4" - - env: REPORTLAB=2.7 - python: "3.3" - - env: REPORTLAB=2.7 - python: "3.4" - - env: REPORTLAB=3.0 - python: "2.6" - - env: REPORTLAB=3.1.44 - python: "2.6" - - env: REPORTLAB=3.2 - python: "2.6" - -install: - - pip install reportlab==$REPORTLAB - - python setup.py install - -script: python setup.py test diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7c5ddfd..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 zhangshine, (c) 2021 Kumi Systems e.U. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9c8317c..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE -include README.rst \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index 446b26a..0000000 --- a/README.rst +++ /dev/null @@ -1,69 +0,0 @@ -========= -PyInvoice -========= - -Invoice/Receipt Generator. - -Dependency ----------- -* Reportlab -* Only tested with Python >3.6 – may work with any Python >2.6 with a corresponding Reportlab version, but no promises. Use Python3! - -Install -------- - -.. code-block:: bash - - pip install git+https://kumig.it/kumisystems/PyInvoice - -Usage ------ - -.. code-block:: python - - from datetime import datetime, date - from pyinvoice.models import InvoiceInfo, ServiceProviderInfo, ClientInfo, Item, Transaction - from pyinvoice.templates import SimpleInvoice - - doc = SimpleInvoice('invoice.pdf') - - # Paid stamp, optional - doc.is_paid = True - - doc.invoice_info = InvoiceInfo(1023, datetime.now(), datetime.now()) # Invoice info, optional - - # Service Provider Info, optional - doc.service_provider_info = ServiceProviderInfo( - name='PyInvoice', - street='My Street', - city='My City', - state='My State', - country='My Country', - post_code='222222', - vat_tax_number='Vat/Tax number' - ) - - # Client info, optional - doc.client_info = ClientInfo(email='client@example.com') - - # Add Item - doc.add_item(Item('Item', 'Item desc', 1, '1.1')) - doc.add_item(Item('Item', 'Item desc', 2, '2.2')) - doc.add_item(Item('Item', 'Item desc', 3, '3.3')) - - # Tax rate, optional - doc.set_item_tax_rate(20) # 20% - - # Transactions detail, optional - doc.add_transaction(Transaction('Paypal', 111, datetime.now(), 1)) - doc.add_transaction(Transaction('Stripe', 222, date.today(), 2)) - - # Optional - doc.set_bottom_tip("Email: example@example.com
Don't hesitate to contact us for any questions.") - - doc.finish() - - -License -------- -MIT diff --git a/dist/invoice.png b/dist/invoice.png new file mode 100644 index 0000000..c1e904d Binary files /dev/null and b/dist/invoice.png differ diff --git a/dist/simple.pdf b/dist/simple.pdf new file mode 100644 index 0000000..34aad3d --- /dev/null +++ b/dist/simple.pdf @@ -0,0 +1,53 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< /F1 2 0 R /F2 3 0 R >> +endobj +2 0 obj +<< /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font >> +endobj +3 0 obj +<< /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font >> +endobj +4 0 obj +<< /Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources << /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] >> /Rotate 0 /Trans << >> + /Type /Page >> +endobj +5 0 obj +<< /Outlines 9 0 R /PageMode /UseNone /Pages 7 0 R /Type /Catalog >> +endobj +6 0 obj +<< /Author (CiCiApp.com) /CreationDate (D:20150614215600-08'00') /Creator (PyInvoice \(https://ciciapp.com/pyinvoice\)) /Keywords () /Producer (ReportLab PDF Library - www.reportlab.com) /Subject (Invoice) + /Title (Invoice) >> +endobj +7 0 obj +<< /Count 1 /Kids [ 4 0 R ] /Type /Pages >> +endobj +8 0 obj +<< /Filter [ /ASCII85Decode /FlateDecode ] /Length 1627 >> +stream +Gau`T9lo&I&A;jKs'[RO3ggS=D\>&f[?F9>D9&X+SoS6b\n/mUCl*2lJ):D1OH`.pML%fK$5r14p?aK:]-%35Ip)a=@Kl8e*%p,(?l\=h>`s[s]N0WZZshY883pEbQZ"mgeCqmP[7V9%%'GJb0&bf`2&EtU[PE*d7.-&!(E2!GMO0+t4n.F@a4[_Ahn?es2)Y2nCFdUUKaU.cIfiKh)6LijsW0Nf`9P14*];W?.hq,(<::O>O'djb\`^jQBk]OUFCl4"J=I5-fS8"^#1Z>OCW4"FZ8Ve"+:b:;iC1_fPQ8JBg.spWXQ&;r*s5JHlY7A..VQi\u#9u]ib$@j<]o$+!_;4/k:/h6eFALA+G/_-PT6!G?H[(Q%AdQ@g23op/_au>649=V.j,;HTB+Xn!4QAHAntYqQ5`lo/HcG+8&iAqm?P(7?UQQ__):($\IINg7-kaFCcZks'-FOuYKX"?nDg.hSFd:cQ(SsMIbp>kKrQXk1Od])R6TOsqpPYY-]bk)2cOki-1!2fr%$+f47'u%B!JM)9A>Eu/\d6gnKPqmhC'3lTJ@h)q5Ag\5U7("NerdeP6itu-7i]ES*]sE4e]3bX?R.dAQsrYqrT&\[^e3H1haB+mJ2:gB/L-]P3*u(hJS7,`orGYhe$GU'M%%gL5f3R'm)R7SXOpC3ntcjqeVH4k:V28B0.RgG=b6o]gUJArM'+`"a/9c)!sKO_U[m\!Gg](ruIu^/e":FI5G3eg-CC3LQ5Ca`%#]Y3Z'.SCFYLhm^(&XP>$[^EYC5mGVZGLYj].Mg?2isendstream +endobj +9 0 obj +<< /Count 0 /Type /Outlines >> +endobj +xref +0 10 +0000000000 65535 f +0000000075 00000 n +0000000119 00000 n +0000000229 00000 n +0000000344 00000 n +0000000541 00000 n +0000000628 00000 n +0000000876 00000 n +0000000938 00000 n +0000002661 00000 n +trailer +<< /ID + % ReportLab generated PDF document -- digest (http://www.reportlab.com) + [(q\364!\275!\347\0005\320<\200\276\267\317\011E) (q\364!\275!\347\0005\320<\200\276\267\317\011E)] + /Info 6 0 R /Root 5 0 R /Size 10 >> +startxref +2710 +%%EOF diff --git a/pyinvoice/__init__.py b/pyinvoice/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyinvoice/components.py b/pyinvoice/components.py deleted file mode 100644 index 0ddff3a..0000000 --- a/pyinvoice/components.py +++ /dev/null @@ -1,58 +0,0 @@ -from reportlab.lib.units import inch -from reportlab.platypus import Paragraph, Table, TableStyle -from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet -from reportlab.lib import colors - - -class CodeSnippet(Paragraph): - style = ParagraphStyle( - name='CodeSnippet', - parent=getSampleStyleSheet()['Code'], - backColor=colors.lightgrey, leftIndent=0, - borderPadding=(5, 5, 5, 5) - ) - - def __init__(self, code): - Paragraph.__init__(self, code, self.style) - - -class SimpleTable(Table): - def __init__(self, data, horizontal_align=None): - Table.__init__(self, data, hAlign=horizontal_align) - - -class TableWithHeader(Table): - def __init__(self, data, horizontal_align=None, style=None): - Table.__init__(self, data, hAlign=horizontal_align) - - default_style = [ - ('INNERGRID', (0, 0), (-1, -1), .25, colors.black), - ('BOX', (0, 0), (-1, -1), .25, colors.black), - ('BACKGROUND', (0, 0), (-1, -len(data)), colors.lightgrey), - ('ALIGN', (0, 0), (-1, -1), 'CENTER'), - ('VALIGN', (0, 0), (-1, -1), 'MIDDLE') - ] - - if style and isinstance(style, list): - default_style.extend(style) - - self.setStyle(TableStyle(default_style)) - - -class PaidStamp(object): - def __init__(self, x, y): - self.x = x - self.y = y - - def __call__(self, canvas, doc): - # "PAID" - canvas.saveState() - canvas.setFontSize(50) - canvas.setFillColor(colors.red) - canvas.setStrokeColor(colors.red) - canvas.rotate(45) - canvas.drawString(self.x, self.y, 'PAID') - canvas.setLineWidth(4) - canvas.setLineJoin(1) # Round join - canvas.rect(self.x - .25 * inch, self.y - .25 * inch, width=2*inch, height=inch) - canvas.restoreState() \ No newline at end of file diff --git a/pyinvoice/models.py b/pyinvoice/models.py deleted file mode 100644 index 7a4d171..0000000 --- a/pyinvoice/models.py +++ /dev/null @@ -1,148 +0,0 @@ -from __future__ import unicode_literals -from decimal import Decimal - - -class PDFInfo(object): - """ - PDF Properties - """ - def __init__(self, title=None, author=None, subject=None): - """ - PDF Properties - :param title: PDF title - :type title: str or unicode - :param author: PDF author - :type author: str or unicode - :param subject: PDF subject - :type subject: str or unicode - """ - self.title = title - self.author = author - self.subject = subject - self.creator = 'PyInvoice (https://ciciapp.com/pyinvoice)' - - -class InvoiceInfo(object): - """ - Invoice information - """ - def __init__(self, invoice_id=None, invoice_datetime=None, due_datetime=None): - """ - Invoice info - :param invoice_id: Invoice id - :type invoice_id: int or str or unicode or None - :param invoice_datetime: Invoice create datetime - :type invoice_datetime: str or unicode or datetime or date - :param due_datetime: Invoice due datetime - :type due_datetime: str or unicode or datetime or date - """ - self.invoice_id = invoice_id - self.invoice_datetime = invoice_datetime - self.due_datetime = due_datetime - - -class AddressInfo(object): - def __init__(self, name=None, street=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 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 int or None - """ - self.name = name - self.street = street - self.city = city - self.state = state - self.country = country - self.post_code = post_code - - -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): - """ - :type name: str or unicode or None - :type street: 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) - self.vat_tax_number = vat_tax_number - - -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): - """ - :type name: str or unicode or None - :type street: 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 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) - self.email = email - self.client_id = client_id - - -class Item(object): - """ - Product/Item information - """ - def __init__(self, name, description, units, unit_price): - """ - Item modal init - :param name: Item name - :type name: str or unicode or int - :param description: Item detail - :type description: str or unicode or int - :param units: Amount - :type units: int or str or unicode - :param unit_price: Unit price - :type unit_price: Decimal or str or unicode or int or float - :return: - """ - self.name = name - self.description = description - self.units = units - self.unit_price = unit_price - - @property - def amount(self): - return int(self.units) * Decimal(str(self.unit_price)) - - -class Transaction(object): - """ - Transaction information - """ - def __init__(self, gateway, transaction_id, transaction_datetime, amount): - """ - :param gateway: Payment gateway like Paypal, Stripe etc. - :type gateway: str or unicode - :param transaction_id: - :type transaction_id: int or str or unicode - :param transaction_datetime: - :type transaction_datetime: date or datetime or str or unicode - :param amount: $$ - :type amount: int or float or str or unicode - :return: - """ - self.gateway = gateway - self.transaction_id = transaction_id - self.transaction_datetime = transaction_datetime - self.amount = amount \ No newline at end of file diff --git a/pyinvoice/templates.py b/pyinvoice/templates.py deleted file mode 100644 index fd4b241..0000000 --- a/pyinvoice/templates.py +++ /dev/null @@ -1,329 +0,0 @@ -from __future__ import unicode_literals -from datetime import datetime, date -from decimal import Decimal - -from reportlab.lib import colors -from reportlab.lib.enums import TA_CENTER, TA_RIGHT -from reportlab.lib.pagesizes import letter -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.lib.units import inch -from reportlab.platypus import SimpleDocTemplate, Paragraph, Table, Spacer, Image - -from pyinvoice.components import SimpleTable, TableWithHeader, PaidStamp -from pyinvoice.models import PDFInfo, Item, Transaction, InvoiceInfo, ServiceProviderInfo, ClientInfo - - -class SimpleInvoice(SimpleDocTemplate): - default_pdf_info = PDFInfo(title='Invoice', author='CiCiApp.com', subject='Invoice') - precision = None - - def __init__(self, invoice_path, pdf_info=None, precision='0.01'): - if not pdf_info: - pdf_info = self.default_pdf_info - - SimpleDocTemplate.__init__( - self, - invoice_path, - pagesize=letter, - rightMargin=inch, - leftMargin=inch, - topMargin=inch, - bottomMargin=inch, - **pdf_info.__dict__ - ) - - self.precision = precision - - self._defined_styles = getSampleStyleSheet() - self._defined_styles.add( - 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) - ) - - self.invoice_info = None - self.service_provider_info = None - self.client_info = None - self.is_paid = False - self._items = [] - self._item_tax_rate = None - self._transactions = [] - self._story = [] - self._bottom_tip = None - self._bottom_tip_align = None - self._logo = None - self._build_kwargs = {} - - @property - def items(self): - return self._items[:] - - def add_item(self, item): - if isinstance(item, Item): - self._items.append(item) - - def set_item_tax_rate(self, rate): - self._item_tax_rate = rate - - @property - def transactions(self): - return self._transactions[:] - - def add_transaction(self, t): - if isinstance(t, Transaction): - self._transactions.append(t) - - def set_bottom_tip(self, text, align=TA_CENTER): - self._bottom_tip = text - self._bottom_tip_align = align - - @staticmethod - def _format_value(value): - if isinstance(value, datetime): - value = value.strftime('%Y-%m-%d %H:%M') - elif isinstance(value, date): - value = value.strftime('%Y-%m-%d') - return value - - def _attribute_to_table_data(self, instance, attribute_verbose_name_list): - data = [] - - for property_name, verbose_name in attribute_verbose_name_list: - attr = getattr(instance, property_name) - if attr: - attr = self._format_value(attr) - data.append(['{0}:'.format(verbose_name), attr]) - - return data - - 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')] - - return self._attribute_to_table_data(self.invoice_info, props) - - return [] - - 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')) - - 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) - - return [] - - 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')) - - 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')] - 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')) - - def _build_service_provider_and_client_info(self): - 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')), '' - ] - ] - table_style = [ - ('SPAN', (0, 0), (1, 0)), - ('SPAN', (3, 0), (4, 0)), - ('LINEBELOW', (0, 0), (1, 0), 1, colors.gray), - ('LINEBELOW', (3, 0), (4, 0), 1, colors.gray), - ('LEFTPADDING', (0, 0), (-1, -1), 0), - ] - client_info_data = self._client_info_data() - service_provider_data = self._service_provider_data() - 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) - else: - service_provider_data.extend([["", ""]*diff]) - for d in zip(service_provider_data, client_info_data): - d[0].append('') - d[0].extend(d[1]) - table_data.append(d[0]) - self._story.append( - Table(table_data, style=table_style) - ) - else: - self._build_service_provider_info() - self._build_client_info() - - def _item_raw_data_and_subtotal(self): - item_data = [] - item_subtotal = 0 - - for item in self._items: - if not isinstance(item, Item): - continue - - item_data.append( - ( - item.name, - Paragraph(item.description, self._defined_styles.get('TableParagraph')), - item.units, - '{0:.2f}'.format(float(item.unit_price)), - '{0:.2f}'.format(item.amount) - ) - ) - item_subtotal += item.amount - - return item_data, item_subtotal - - def _item_data_and_style(self): - # Items - item_data, item_subtotal = self._item_raw_data_and_subtotal() - style = [] - - if not item_data: - return item_data, style - - self._story.append( - Paragraph('Detail', self._defined_styles.get('Heading1')) - ) - - item_data_title = ('Name', 'Description', 'Units', 'Unit Price', 'Amount') - item_data.insert(0, item_data_title) # Insert title - - # Summary field - sum_start_y_index = len(item_data) - sum_end_x_index = -1 - 1 - sum_start_x_index = len(item_data_title) - abs(sum_end_x_index) - - # ##### Subtotal ##### - rounditem_subtotal = self.getroundeddecimal(item_subtotal, self.precision) - item_data.append( - ('Subtotal', '', '', '', '{0:.2f}'.format(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')) - - # Tax total - if self._item_tax_rate is not None: - 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), '', '', '', '{0:.2f}'.format(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')) - else: - tax_total = None - - # Total - total = item_subtotal + (tax_total if tax_total else Decimal('0')) - roundtotal = self.getroundeddecimal(total, self.precision) - item_data.append(('Total', '', '', '', '{0:.2f}'.format(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')) - - return item_data, style - - def getroundeddecimal(self, nrtoround, precision): - d = Decimal(nrtoround) - aftercomma = Decimal(precision) # or anything that has the exponent depth you want - 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)) - - def _transactions_data(self): - transaction_table_data = [ - ( - t.transaction_id, - Paragraph(t.gateway, self._defined_styles.get('TableParagraph')), - self._format_value(t.transaction_datetime), - '{0:.2f}'.format(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')) - - return transaction_table_data - - def _build_transactions(self): - # Transaction - 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')) - - def _build_bottom_tip(self): - if self._bottom_tip: - self._story.append(Spacer(5, 5)) - self._story.append( - Paragraph( - self._bottom_tip, - ParagraphStyle( - 'BottomTip', - parent=self._defined_styles.get('Normal'), - alignment=self._bottom_tip_align - ) - ) - ) - - def logo(self, logo): - if isinstance(logo, Image): - self._logo = logo - else: - self._logo = Image(logo) - - def _build_logo(self): - if self._logo: - self._logo.hAlign = "LEFT" - self._logo.vAlign = "TOP" - - self._story.append(self._logo) - - def _build_paid_stamp(self): - if self.is_paid: - self._build_kwargs['onFirstPage'] = PaidStamp(3 * inch, -2 * inch) - - def finish(self): - self._build_logo() - self._build_invoice_info() - self._build_service_provider_and_client_info() - self._build_items() - self._build_transactions() - self._build_bottom_tip() - self._build_paid_stamp() - - self.build(self._story, **self._build_kwargs) \ No newline at end of file diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 5db72dd..0000000 --- a/renovate.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ] -} diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 5bf8348..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -reportlab diff --git a/setup.py b/setup.py deleted file mode 100644 index 00ed041..0000000 --- a/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -import os - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - - -with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: - README = readme.read() - -# allow setup.py to be run from any path -os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) - - -setup( - name='PyInvoice', - version='0.1.8', - packages=['pyinvoice', 'tests'], - include_package_data=True, - license='MIT License', - description='Invoice/Receipt generator', - long_description=README, - url='https://kumig.it/kumisystems/PyInvoice', - author='Kumi Systems e.U.', - author_email='support@kumi.systems', - install_requires=['reportlab'], - test_suite='tests', - classifiers=[ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: Implementation :: CPython', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/dist/empty.txt b/tests/fixtures/dist/empty.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_templates.py b/tests/test_templates.py deleted file mode 100644 index 2e69eb9..0000000 --- a/tests/test_templates.py +++ /dev/null @@ -1,287 +0,0 @@ -from decimal import Decimal -import os -import unittest -from datetime import datetime, date - -from pyinvoice.models import InvoiceInfo, ServiceProviderInfo, ClientInfo, Item, Transaction -from pyinvoice.templates import SimpleInvoice - - -class TestSimpleInvoice(unittest.TestCase): - def setUp(self): - self.file_base_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fixtures/dist') - - def test_simple(self): - invoice_path = os.path.join(self.file_base_dir, 'simple.pdf') - - if os.path.exists(invoice_path): - os.remove(invoice_path) - - doc = SimpleInvoice(invoice_path) - - doc.is_paid = True - - doc.invoice_info = InvoiceInfo(1023, datetime.now(), datetime.now()) - - doc.service_provider_info = ServiceProviderInfo( - name='PyInvoice', - street='My Street', - city='My City', - state='My State', - country='My Country', - post_code='222222', - vat_tax_number='Vat/Tax number' - ) - - doc.client_info = ClientInfo(email='client@example.com') - - doc.add_item(Item('Item', 'Item desc', 1, '1.1')) - doc.add_item(Item('Item', 'Item desc', 2, '2.2')) - doc.add_item(Item('Item', 'Item desc', 3, '3.3')) - - items = doc.items - self.assertEqual(len(items), 3) - - doc.set_item_tax_rate(20) # 20% - - doc.add_transaction(Transaction('Paypal', 111, datetime.now(), 1)) - doc.add_transaction(Transaction('Stripe', 222, date.today(), 2)) - - transactions = doc.transactions - self.assertEqual(len(transactions), 2) - - doc.set_bottom_tip("Email: example@example.com
Don't hesitate to contact us for any questions.") - - doc.finish() - - self.assertTrue(os.path.exists(invoice_path)) - - def test_only_items(self): - invoice_path = os.path.join(self.file_base_dir, 'only_items.pdf') - if os.path.exists(invoice_path): - os.remove(invoice_path) - - invoice = SimpleInvoice(invoice_path) - - # Before add items - item_data, item_subtotal = invoice._item_raw_data_and_subtotal() - self.assertEqual(len(item_data), 0) - self.assertEqual(item_subtotal, Decimal('0')) - item_data, style = invoice._item_data_and_style() - self.assertEqual(len(item_data), 0) - self.assertEqual(style, []) - - # Add items - invoice.add_item(Item('Item1', 'Item desc', 1, 1.1)) - invoice.add_item(Item('Item2', 'Item desc', 2, u'2.2')) - invoice.add_item(Item(u'Item3', 'Item desc', 3, '3.3')) - - # After add items - items = invoice.items - self.assertEqual(len(items), 3) - self.assertEqual(items[0].name, 'Item1') - self.assertEqual(items[0].amount, Decimal('1.1')) - self.assertEqual(items[1].amount, Decimal('4.4')) - self.assertEqual(items[2].name, u'Item3') - self.assertEqual(items[2].amount, Decimal('9.9')) - - item_data, item_subtotal = invoice._item_raw_data_and_subtotal() - self.assertEqual(item_subtotal, Decimal('15.4')) - self.assertEqual(len(item_data), 3) - - item_data, style = invoice._item_data_and_style() - self.assertEqual(len(item_data), 6) # header, subtotal, total - self.assertEqual(item_data[-2][-1], Decimal('15.4')) # subtotal - self.assertEqual(item_data[-1][-1], Decimal('15.4')) # total - - # test style - # ## Subtotal - self.assertEqual(style[-4], ('SPAN', (0, 4), (3, 4))) - self.assertEqual(style[-3], ('ALIGN', (0, 4), (-2, -1), 'RIGHT')) - # ## Total - self.assertEqual(style[-2], ('SPAN', (0, 5), (3, 5))) - self.assertEqual(style[-1], ('ALIGN', (0, 5), (-2, -1), 'RIGHT')) - - invoice.finish() - - self.assertTrue(os.path.exists(invoice_path)) - - def test_only_items_with_tax_rate(self): - invoice_path = os.path.join(self.file_base_dir, 'only_items_with_tax.pdf') - if os.path.exists(invoice_path): - os.remove(invoice_path) - - invoice = SimpleInvoice(invoice_path) - - # Before add items - item_data, item_subtotal = invoice._item_raw_data_and_subtotal() - self.assertEqual(len(item_data), 0) - self.assertEqual(item_subtotal, Decimal('0')) - item_data, style = invoice._item_data_and_style() - self.assertEqual(len(item_data), 0) - self.assertEqual(style, []) - - # Add items - invoice.add_item(Item('Item1', 'Item desc', 1, 1.1)) - invoice.add_item(Item('Item2', 'Item desc', 2, u'2.2')) - invoice.add_item(Item(u'Item3', 'Item desc', 3, '3.3')) - # set tax rate - invoice.set_item_tax_rate(19) - - # After add items - items = invoice.items - self.assertEqual(len(items), 3) - self.assertEqual(items[0].name, 'Item1') - self.assertEqual(items[0].amount, Decimal('1.1')) - self.assertEqual(items[1].amount, Decimal('4.4')) - self.assertEqual(items[2].name, u'Item3') - self.assertEqual(items[2].amount, Decimal('9.9')) - - item_data, item_subtotal = invoice._item_raw_data_and_subtotal() - self.assertEqual(item_subtotal, Decimal('15.4')) - self.assertEqual(len(item_data), 3) - - item_data, style = invoice._item_data_and_style() - self.assertEqual(len(item_data), 7) # header, subtotal, tax, total - self.assertEqual(item_data[-3][-1], Decimal('15.4')) # subtotal - self.assertEqual(item_data[-2][-1], Decimal('2.93')) # tax - self.assertEqual(item_data[-1][-1], Decimal('18.33')) # total - - invoice.finish() - - self.assertTrue(os.path.exists(invoice_path)) - - def test_invoice_info(self): - invoice_path = os.path.join(self.file_base_dir, 'invoice_info.pdf') - if os.path.exists(invoice_path): - os.remove(invoice_path) - - invoice = SimpleInvoice(invoice_path) - - # Before add invoice info - invoice_info_data = invoice._invoice_info_data() - self.assertEqual(invoice_info_data, []) - - invoice.invoice_info = InvoiceInfo(12) - - # After add invoice info - invoice_info_data = invoice._invoice_info_data() - self.assertEqual(len(invoice_info_data), 1) - self.assertEqual(invoice_info_data[0][0], 'Invoice id:') - self.assertEqual(invoice_info_data[0][1], 12) - - invoice.invoice_info = InvoiceInfo(12, invoice_datetime=datetime(2015, 6, 1)) - invoice_info_data = invoice._invoice_info_data() - self.assertEqual(len(invoice_info_data), 2) - self.assertEqual(invoice_info_data[1][0], 'Invoice date:') - self.assertEqual(invoice_info_data[1][1], '2015-06-01 00:00') - - 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): - 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', - street='Street xxx', - 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[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_client_info(self): - invoice_path = os.path.join(self.file_base_dir, 'client_info.pdf') - if os.path.exists(invoice_path): - os.remove(invoice_path) - - invoice = SimpleInvoice(invoice_path) - - # Before add client info - info_data = invoice._client_info_data() - self.assertEqual(info_data, []) - - # Empty info - invoice.client_info = ClientInfo() - info_data = invoice._client_info_data() - self.assertEqual(info_data, []) - - invoice.client_info = ClientInfo( - name='Client ccc', - street='Street sss', - city='City ccc', - state='State sss', - country='Country ccc', - post_code='Post code ppp', - email='Email@example.com', - client_id=3214 - ) - - # After add client info - info_data = invoice._client_info_data() - self.assertEqual(len(info_data), 8) - self.assertEqual(info_data[0][0], 'Name:') - self.assertEqual(info_data[0][1], 'Client ccc') - self.assertEqual(info_data[6][0], 'Email:') - self.assertEqual(info_data[6][1], 'Email@example.com') - self.assertEqual(info_data[7][0], 'Client id:') - self.assertEqual(info_data[7][1], 3214) - - invoice.finish() - - self.assertTrue(os.path.exists(invoice_path)) - - def test_transaction(self): - invoice_path = os.path.join(self.file_base_dir, 'transaction.pdf') - if os.path.exists(invoice_path): - os.remove(invoice_path) - - invoice = SimpleInvoice(invoice_path) - - transaction_data = invoice._transactions_data() - self.assertEqual(transaction_data, []) - - invoice.add_transaction(Transaction('A', 1, date.today(), 9.9)) - invoice.add_transaction(Transaction('B', 3, date(2015, 6, 1), 3.3)) - - transaction_data = invoice._transactions_data() - self.assertEqual(len(transaction_data), 3) - self.assertEqual(transaction_data[0][0], 'Transaction id') - self.assertEqual(transaction_data[1][3], 9.9) - self.assertEqual(transaction_data[2][0], 3) - self.assertEqual(transaction_data[2][2], '2015-06-01') - self.assertEqual(transaction_data[2][3], 3.3) - - invoice.finish() - - self.assertTrue(os.path.exists(invoice_path))