summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEkaitz Zarraga <ekaitz@elenq.tech>2020-08-08 14:48:10 +0200
committerEkaitz Zarraga <ekaitz@elenq.tech>2020-08-08 14:48:10 +0200
commit2ccbf8b690c4ea052ec3388bfabeb4b6943a74db (patch)
tree1be225f631fa600592f0ffcf957a95cabd79bf8a
parentaf0515ac909db46ec1fedc8f3cf9d884ff21ce31 (diff)
Basic output control: summary, latex and json
-rw-r--r--Pipfile12
-rw-r--r--Pipfile.lock67
-rw-r--r--README.md3
-rw-r--r--fracture/__main__.py17
-rw-r--r--fracture/db.py20
-rw-r--r--fracture/invoice.py121
6 files changed, 164 insertions, 76 deletions
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..d0d7eed
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,12 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+
+[packages]
+jinja2 = "*"
+
+[requires]
+python_version = "3.8"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000..0d8fbd5
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,67 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "b64f541faac077e16f6555f7fc19dcbdba1044f39dd62401a02e10bcf4092f25"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.8"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "jinja2": {
+ "hashes": [
+ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
+ "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
+ ],
+ "index": "pypi",
+ "version": "==2.11.2"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
+ "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
+ "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
+ "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
+ "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
+ "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
+ "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
+ "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
+ "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
+ "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
+ "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
+ "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
+ "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
+ "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
+ "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
+ "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
+ "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
+ "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
+ "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
+ "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
+ "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
+ "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
+ "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
+ "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
+ "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
+ "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
+ "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
+ "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
+ "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
+ "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
+ "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
+ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
+ "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
+ ],
+ "version": "==1.1.1"
+ }
+ },
+ "develop": {}
+}
diff --git a/README.md b/README.md
index ac31b25..5edb28a 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,3 @@ store the invoice in the database.
## TODO
-- Invoice rendering in an cool format (LaTeX for PDF rendering?)
-- Dump summary that conforms government standards
- - Check what amount of detail it needs
diff --git a/fracture/__main__.py b/fracture/__main__.py
index ee26bbe..c99b0e9 100644
--- a/fracture/__main__.py
+++ b/fracture/__main__.py
@@ -1,5 +1,6 @@
from invoice import Invoice, Tax, Product, Conf
import db
+from jinja2 import Template
from csv import DictWriter
import datetime
@@ -139,11 +140,19 @@ def summarize(xlsx=False, year=None, quarter=None):
wrtr.writerow(r)
@command
-def render(id, type=None):
- invoice = Invoice.load_by_idrepr(id, type)
- if invoice is not None:
+def render(id, type=None, format=None):
+ invoice = Invoice.load(id, type)
+ if format == "json":
print(invoice.to_json())
+ if format == "xelatex":
+ # TODO AUTOMATE TEMPLATE SEARCH AND THAT, THIS IS THIS FOR DEV
+ with open("templates/template.tex", "r") as f:
+ template_text = f.read()
+ template = Template(template_text)
+ if invoice is not None:
+ print(template.render(invoice=invoice.to_dict()))
+
if __name__ == "__main__":
load_config()
@@ -181,6 +190,8 @@ if __name__ == "__main__":
help="Invoice identification string")
summary_parser.add_argument("--type", type=str,
help="Invoice type", default="sent")
+ summary_parser.add_argument("--format", type=str,
+ choices={"json", "xelatex"}, help="Invoice type", default="json")
summary_parser.set_defaults(func=render)
diff --git a/fracture/db.py b/fracture/db.py
index 1e90727..9ac0341 100644
--- a/fracture/db.py
+++ b/fracture/db.py
@@ -5,26 +5,23 @@ def create(dbname):
c = conn.cursor()
c.execute('''CREATE TABLE products
- ( invoice_id INTEGER,
+ ( invoice_id TEXT,
invoice_type TEXT,
- invoice_series INTEGER,
description TEXT NOT NULL,
units REAL NOT NULL,
price_unit REAL NOT NULL,
vat REAL,
PRIMARY KEY(description,
invoice_id,
- invoice_type,
- invoice_series)
+ invoice_type)
FOREIGN KEY(invoice_id) REFERENCES invoices(id)
FOREIGN KEY(invoice_type) REFERENCES invoices(type)
- FOREIGN KEY(invoice_series) REFERENCES invoices(series)
ON DELETE CASCADE )''')
# date is %Y-%m-%d
c.execute('''CREATE TABLE invoices
- ( id INTEGER NOT NULL,
- id_repr TEXT NOT NULL,
+ ( number INTEGER NOT NULL,
+ id TEXT NOT NULL,
type TEXT NOT NULL,
series INTEGER NOT NULL,
date TEXT NOT NULL,
@@ -34,22 +31,19 @@ def create(dbname):
customer_name TEXT,
customer_address TEXT,
PRIMARY KEY(type, series, id),
- UNIQUE (id_repr, type)
+ UNIQUE (id, type)
)''')
c.execute('''CREATE TABLE taxes
( name TEXT NOT NULL,
- invoice_id INTEGER,
+ invoice_id TEXT,
invoice_type TEXT,
- invoice_series INTEGER,
ratio REAL,
PRIMARY KEY(name,
invoice_id,
- invoice_type,
- invoice_series)
+ invoice_type)
FOREIGN KEY(invoice_id) REFERENCES invoices(id)
FOREIGN KEY(invoice_type) REFERENCES invoices(type)
- FOREIGN KEY(invoice_series) REFERENCES invoices(series)
ON DELETE CASCADE )''')
conn.commit()
diff --git a/fracture/invoice.py b/fracture/invoice.py
index e23d257..d795340 100644
--- a/fracture/invoice.py
+++ b/fracture/invoice.py
@@ -9,6 +9,10 @@ class Conf:
# MOVE ALL THE CONFIG HERE
CURRENCY = None
CURRENCY_DECIMAL = 0
+ @classmethod
+ def round(cls, num):
+ return round(num, cls.CURRENCY_DECIMAL)
+
class Tax:
def __init__(self, name="", ratio = 0.0):
@@ -34,16 +38,24 @@ class Product:
else:
raise ValueError("Product: No valid VAT. Valid are:"+\
str(Product.VATS))
- def calc_base(self):
+ @property
+ def total(self):
+ return self.base+self.charged_vat
+ @property
+ def base(self):
return self.price_unit * self.units
- def calc_charged_vat(self):
- return self.calc_base() * self.vat
+ @property
+ def charged_vat(self):
+ return self.base * self.vat
def to_dict(self):
return {
"description": self.description,
"units": self.units,
- "price-unit": round(self.price_unit, Conf.CURRENCY_DECIMAL),
- "vat": self.vat
+ "price-unit": Conf.round(self.price_unit),
+ "vat": self.vat,
+ "vat-charged": Conf.round(self.charged_vat),
+ "base": Conf.round(self.base),
+ "total": Conf.round(self.total)
}
class Customer:
@@ -71,10 +83,10 @@ class Invoice:
products = None,
customer = None,
taxes = None,
- id = None):
+ number = None):
# Initializes to empty state if not set
self.set_type(type or tuple(Invoice._TYPES)[0])
- self.id = id
+ self.number = number
self.set_series(series or tuple(Invoice.SERIES.keys())[0])
self.date = idate or date.today()
self.notes = notes or ""
@@ -96,8 +108,9 @@ class Invoice:
% Invoice._TYPES)
self.type = type
- def format_id(self):
- return Invoice.ID_FORMAT(self.series, self.date, self.id)
+ @property
+ def id(self):
+ return Invoice.ID_FORMAT(self.series, self.date, self.number)
def __repr__(self):
return "<Invoice object>\n\t"+\
@@ -180,21 +193,10 @@ class Invoice:
@classmethod
- def load_by_idrepr(cls, id_repr, type="sent"):
- with sqlite3.connect(cls.DB_FILE) as conn:
- conn.row_factory = sqlite3.Row
-
- c = conn.cursor()
- c.execute("""SELECT id, series, type FROM invoices
- WHERE id_repr = ? AND type = ?""", (id_repr, type))
- res = c.fetchone()
- if res is None:
- return None
- return cls.load(res["id"], res["series"], type)
-
- @classmethod
- def load(cls, id, series, type="sent"):
+ def load(cls, id, type="sent", debug=False):
with sqlite3.connect(cls.DB_FILE) as conn:
+ if debug:
+ conn.set_trace_callback(print)
conn.row_factory = sqlite3.Row
c = conn.cursor()
@@ -202,8 +204,7 @@ class Invoice:
# PRODUCTS
products = ()
c.execute("""SELECT * FROM products WHERE invoice_id = ?
- AND invoice_series = ?
- AND invoice_type = ?""", (id, series, type))
+ AND invoice_type = ?""", (id, type))
res = c.fetchone()
while res is not None:
desc = res["description"]
@@ -216,8 +217,7 @@ class Invoice:
# TAXES
taxes = ()
c.execute("""SELECT * FROM taxes WHERE invoice_id = ?
- AND invoice_series = ?
- AND invoice_type = ?""", (id, series, type))
+ AND invoice_type = ?""", (id, type))
res = c.fetchone()
while res is not None:
taxes += (Tax(res["name"], res["ratio"]) ,)
@@ -225,11 +225,10 @@ class Invoice:
# INVOICE
c.execute("""SELECT * FROM invoices WHERE id = ?
- AND series = ?
- AND type = ?""", (id, series, type))
+ AND type = ?""", (id, type))
res = c.fetchone()
return cls(
- id = id,
+ number = res["number"],
type = res["type"],
series = res["series"],
idate = datetime.strptime(res["date"],"%Y-%m-%d").date(),
@@ -249,13 +248,12 @@ class Invoice:
conn.row_factory = sqlite3.Row
c = conn.cursor()
- c.execute(("SELECT id, series, type FROM invoices " + cond), vals)
- return tuple( cls.load( int(r["id"]),
- int(r["series"]),
- r["type"] ) for r in c.fetchall() )
+ c.execute(("SELECT id, type FROM invoices " + cond), vals)
+ return tuple( cls.load( r["id"], r["type"] ) \
+ for r in c.fetchall() )
@classmethod
- def latest_id(cls, series, type, debug=False):
+ def latest_number(cls, series, type, debug=False):
with sqlite3.connect(cls.DB_FILE) as conn:
if debug:
conn.set_trace_callback(print)
@@ -263,15 +261,15 @@ class Invoice:
conn.row_factory = sqlite3.Row
c = conn.cursor()
- c.execute("""SELECT id FROM invoices
+ c.execute("""SELECT number FROM invoices
WHERE series = ? AND type = ?
- ORDER BY(id) DESC LIMIT 1""", (series, type))
+ ORDER BY(number) DESC LIMIT 1""", (series, type))
res = c.fetchall()
- latest_id = 0
+ latest_number = 0
if len(res) != 0:
- latest_id = int(res[0]["id"])
- return latest_id
+ latest_number = int(res[0]["number"])
+ return latest_number
@classmethod
def load_by_date(cls, year, quarter=None):
@@ -297,14 +295,14 @@ class Invoice:
def persist(self):
conn = sqlite3.connect(Invoice.DB_FILE)
- if self.id is None:
- self.id = self.latest_id(self.series, self.type) + 1
+ if self.number is None:
+ self.number = self.latest_number(self.series, self.type) + 1
c = conn.cursor()
c.execute("""INSERT INTO invoices (
type,
series,
+ number,
id,
- id_repr,
date,
notes,
customer_id,
@@ -313,8 +311,8 @@ class Invoice:
) VALUES (?,?,?,?,?,?,?,?,?)""", (
self.type,
self.series,
+ self.number,
self.id,
- self.format_id(),
self.date.strftime("%Y-%m-%d"),
self.notes,
self.customer.id,
@@ -324,15 +322,13 @@ class Invoice:
for p in self.products:
c.execute("""INSERT INTO products (
invoice_id,
- invoice_series,
invoice_type,
description,
units,
price_unit,
vat
- ) VALUES (?,?,?,?,?,?,?)""",(
+ ) VALUES (?,?,?,?,?,?)""",(
self.id,
- self.series,
self.type,
p.description,
p.units,
@@ -342,36 +338,47 @@ class Invoice:
for x in self.taxes:
c.execute("""INSERT INTO taxes (
invoice_id,
- invoice_series,
invoice_type,
name,
ratio
- ) VALUES (?,?,?,?,?)""", (
+ ) VALUES (?,?,?,?)""", (
self.id,
- self.series,
self.type,
x.name,
x.ratio))
conn.commit()
conn.close()
- return self.id
+ return (self.id, self.type)
+ @property
+ def vat_charged(self):
+ return sum(p.charged_vat for p in self.products)
+ @property
+ def base(self):
+ return sum(p.base for p in self.products)
+ @property
+ def total(self):
+ return sum(p.total for p in self.products) + \
+ sum(self.base*t.ratio for t in self.taxes)
def to_dict(self):
return {
"products": tuple(p.to_dict() for p in self.products),
- "taxes": tuple(t.to_dict() for t in self.taxes),
+ "taxes": tuple(dict(t.to_dict(), applied=Conf.round(self.base*t.ratio)) for t in self.taxes),
"type": self.type,
- "id": self.format_id(),
+ "id": self.id,
"date": self.date.strftime("%Y-%m-%d"),
"customer": {
"id": self.customer.id,
"name": self.customer.name,
"address": self.customer.address,
},
+ "base": self.base,
"notes": self.notes,
+ "total": Conf.round(self.total),
+ "vat-charged": self.vat_charged
}
def to_json(self):
return json.dumps(self.to_dict())
@@ -380,7 +387,7 @@ class Invoice:
row = OrderedDict(
type = self.type,
- id = self.format_id(),
+ id = self.id,
date = self.date.strftime("%Y-%m-%d"),
customer_id = self.customer.id,
customer_name = self.customer.name,
@@ -398,9 +405,9 @@ class Invoice:
vat_base = 0
charged_vat = 0
for product in ps:
- vat_base += product.calc_base()
- charged_vat += product.calc_charged_vat()
- row[to_base_key(vat)] = round(vat_base, Conf.CURRENCY_DECIMAL)
+ vat_base += product.base
+ charged_vat += product.charged_vat
+ row[to_base_key(vat)] = round(vat_base, Conf.CURRENCY_DECIMAL)
row[to_vat_key(vat)] = round(charged_vat, Conf.CURRENCY_DECIMAL)
total_base += vat_base