diff options
Diffstat (limited to 'fracture')
-rw-r--r-- | fracture/__main__.py | 17 | ||||
-rw-r--r-- | fracture/db.py | 20 | ||||
-rw-r--r-- | fracture/invoice.py | 121 |
3 files changed, 85 insertions, 73 deletions
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 |