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
62
.gitignore
vendored
|
@ -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
|
|
37
.travis.yml
37
.travis.yml
|
@ -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
21
LICENSE
|
@ -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.
|
|
|
@ -1,2 +0,0 @@
|
||||||
include LICENSE
|
|
||||||
include README.rst
|
|
69
README.rst
69
README.rst
|
@ -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
BIN
dist/invoice.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
53
dist/simple.pdf
vendored
Normal file
53
dist/simple.pdf
vendored
Normal 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
|
|
@ -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()
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -1 +0,0 @@
|
||||||
reportlab
|
|
44
setup.py
44
setup.py
|
@ -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',
|
|
||||||
],
|
|
||||||
)
|
|
0
tests/fixtures/dist/empty.txt
vendored
0
tests/fixtures/dist/empty.txt
vendored
|
@ -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))
|
|
Loading…
Reference in a new issue