Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

16 changed files with 53 additions and 1058 deletions

62
.gitignore vendored
View file

@ -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

View file

@ -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

21
LICENSE
View file

@ -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.

View file

@ -1,2 +0,0 @@
include LICENSE
include README.rst

View file

@ -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<br />Don't hesitate to contact us for any questions.")
doc.finish()
License
-------
MIT

BIN
dist/invoice.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

53
dist/simple.pdf vendored Normal file
View file

@ -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\<Q^a,$rrsdi/Vj#0c`6/iXPe$%JJ&9s4n"pb]d"KdL_K/0W8"@_J=lrj8.RC@nBUH!]io]q2lPD8T!qVbd)@9cQL+h"I#r:$RN'S_Q&eY\D*,_[<O#>=h>`s[s]N0WZZshY883pEbQZ"mgeCqmP[7V9%%'GJb0&bf<q6VaiH/LI4P?MQ?%lF_@M#f2unib7*H^tOiE+"H_7L.GoH_jOi?ZhpdJJq*^")!jV#YkI<HV5ut.N`!H:Z.J:):^MiJ[8EM7hr!$olm=t:noK]HT[3@Q2!G_Xds2$j5ul&E,h@cSubdJS+T^bbVrgZNa690Q7/K;![d32F:8H]Xp*cuU<_nBF^O3@295iKbQ[KsC__prRZKpAXr_r]I'Cp-n;V8phh,TZo1Qf#A]9'k^JKm9(;$O4qBc0-XWHnOZ<4:So-bHbF`+6SSl]FaH1::Ki:%uB3V\\U;1pkV.]8@R24(A<H6/jKTh*q47n>`2&EtU[PE*d7.-&!(E2!GMO0+t4n.F@a4[_Ahn?es2)Y2n<H<4.:;0RAoFgmho6=$lOl;<54*gP39I;d]k$5VshW7ODFY5aFlkG*ekMX\QZEp?U"hcY(^cb;]U>CFdUUKaU.cIfiKh)6LijsW0<n;RTKPSqa(n7as&B%rBPXs=odD*#W8h:pe]m,iJLO@m"1hElsIF%X5j?=@/'/ckjM$SLbq8]h^Gd$B<^<>Nf`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%$<U\o)ZE(UPUQ"Yt9dGO.m[fN=0iV<CZG3dR)DZ3:P1Z'l8,o+2sRY&20\o&7:kl-2et#1kQuO,GEjXenAe4).p@P?(l`5<&U%_]^FAqpY)`qM'1NoghTD,1e%X"HhZ)'dCq)5ekU4;FFT*i;,2R,M&#Zaqu=W_PEjm=EEJ_,<,frTWjCo_s&!CuW&Aku%QPr'Z"f31%W>+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-CC3LQ<b>5Ca`%#]Y3Z'.SCFYLhm^(&XP>$[^EYC5mGVZGLYj].Mg?2is<Nn@6ZNp)n9FgD4a&N&fZ40V/+H\@%m*~>endstream
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

View file

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -1 +0,0 @@
reportlab

View file

@ -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',
],
)

View file

View file

View file

@ -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<br />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))