diff options
-rw-r--r-- | Pipfile | 12 | ||||
-rw-r--r-- | Pipfile.lock | 67 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | fracture/__main__.py | 17 | ||||
-rw-r--r-- | fracture/db.py | 20 | ||||
-rw-r--r-- | fracture/invoice.py | 121 |
6 files changed, 164 insertions, 76 deletions
@@ -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": {} +} @@ -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 |