Compare commits
No commits in common. "gh-pages" and "master" have entirely different histories.
16 changed files with 1058 additions and 53 deletions
62
.gitignore
vendored
Normal file
62
.gitignore
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
# 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
|
37
.travis.yml
Normal file
37
.travis.yml
Normal file
|
@ -0,0 +1,37 @@
|
|||
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
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
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.
|
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
|
@ -0,0 +1,2 @@
|
|||
include LICENSE
|
||||
include README.rst
|
69
README.rst
Normal file
69
README.rst
Normal file
|
@ -0,0 +1,69 @@
|
|||
=========
|
||||
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
BIN
dist/invoice.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 97 KiB |
53
dist/simple.pdf
vendored
53
dist/simple.pdf
vendored
|
@ -1,53 +0,0 @@
|
|||
%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
|
0
pyinvoice/__init__.py
Normal file
0
pyinvoice/__init__.py
Normal file
58
pyinvoice/components.py
Normal file
58
pyinvoice/components.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
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()
|
148
pyinvoice/models.py
Normal file
148
pyinvoice/models.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
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
|
329
pyinvoice/templates.py
Normal file
329
pyinvoice/templates.py
Normal file
|
@ -0,0 +1,329 @@
|
|||
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)
|
1
requirements-dev.txt
Normal file
1
requirements-dev.txt
Normal file
|
@ -0,0 +1 @@
|
|||
reportlab
|
44
setup.py
Normal file
44
setup.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
#!/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',
|
||||
],
|
||||
)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/fixtures/dist/empty.txt
vendored
Normal file
0
tests/fixtures/dist/empty.txt
vendored
Normal file
287
tests/test_templates.py
Normal file
287
tests/test_templates.py
Normal file
|
@ -0,0 +1,287 @@
|
|||
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))
|
Loading…
Reference in a new issue