import json import sqlite3 from datetime import date, datetime, timedelta from configparser import ConfigParser from collections import OrderedDict import io class Conf: # MOVE ALL THE CONFIG HERE CURRENCY = None CURRENCY_DECIMAL = 0 class Tax: def __init__(self, name="", ratio = 0.0): self.name = name.upper() self.ratio = ratio def to_dict(self): return { "name": self.name, "ratio": self.ratio, } class Product: VATS = () def __init__(self, description, units=1.0, price_unit=0.0, vat = 0.21): if len(description) == 0: raise ValueError("Product: No description provided") self.description = description self.units = units self.price_unit = price_unit if vat in self.VATS: self.vat = vat else: raise ValueError("Product: No valid VAT. Valid are:"+\ str(Product.VATS)) def calc_base(self): return self.price_unit * self.units def calc_charged_vat(self): return self.calc_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 } class Customer: def __init__(self, name, id = "", address = ""): if len(name) == 0: raise ValueError("Customer: No name provided") self.name = name self.id = id self.address = address class Invoice: _TYPES = {"sent", "received"} SERIES = {} ID_FORMAT = None DB_FILE = None DEFAULT_TAXES = () DEFAULT_PRODUCT_DESC = "Empty Product" DEFAULT_CUSTOMER_DESC = "Empty Customer" def __init__(self, type = None, series = None, idate = None, notes= None, products = None, customer = None, taxes = None, id = None): # Initializes to empty state if not set self.set_type(type or tuple(Invoice._TYPES)[0]) self.id = id self.set_series(series or tuple(Invoice.SERIES.keys())[0]) self.date = idate or date.today() self.notes = notes or "" self.products = products or (Product(Invoice.DEFAULT_PRODUCT_DESC), Product(Invoice.DEFAULT_PRODUCT_DESC)) self.customer = customer or Customer(Invoice.DEFAULT_CUSTOMER_DESC) self.taxes = taxes or Invoice.DEFAULT_TAXES def set_series(self, series): """ Series is an integer """ if series not in Invoice.SERIES.keys(): raise ValueError ("Not valid series for Invoice. Valid are %s" % Invoice.SERIES) self.series = series def set_type(self, type): if type not in Invoice._TYPES: raise ValueError ("Not valid type for Invoice. Valid are: %s" % Invoice._TYPES) self.type = type def format_id(self): return Invoice.ID_FORMAT(self.series, self.date, self.id) def __repr__(self): return "\n\t"+\ self.to_config().replace("\n", "\n\t") + \ "\n" def to_config(self): strf = io.StringIO() cfg = ConfigParser() cfg["invoice"] = { "series": self.series, "date": self.date, "type": self.type, "notes": self.notes, "customer-name": self.customer.name, "customer-address": self.customer.address, "customer-id": self.customer.id, } for i,p in enumerate(self.products): cfg["product-"+str(i)] = { "description": p.description, "units": p.units, "price_unit": p.price_unit, "vat": p.vat } cfg["taxes"] = { x.name: x.ratio for x in self.taxes } cfg.write(strf) return strf.getvalue() @classmethod def from_config(cls, config): cfg = ConfigParser() cfg.read_string(config) # PRODUCTS SECTIONS products = () for s in cfg.sections(): if not s.startswith("product"): continue desc = cfg[s]["description"] un = float(cfg[s]["units"]) pu = float(cfg[s]["price_unit"]) vat = float(cfg[s]["vat"]) products += ( Product(desc,un,pu,vat) ,) if cfg[s]["description"] == cls.DEFAULT_PRODUCT_DESC: raise ValueError("Product name not set") if len(products) == 0: raise ValueError("No products assigned") # TAXES SECTION taxes = () if cfg.has_section("taxes"): for x in cfg["taxes"]: taxes += ( Tax(x, float(cfg["taxes"][x])) ,) # INVOICE SECTION if not cfg.has_section("invoice"): raise ValueError("[invoice] section needed") i = cfg["invoice"] series = int(i["series"]) date = datetime.strptime(i["date"], "%Y-%m-%d").date() notes = i["notes"] type = i["type"] customer = Customer( i["customer-name"], i["customer-address"], i["customer-id"] ) if i["customer-name"] == cls.DEFAULT_CUSTOMER_DESC: raise ValueError("Customer name not set") return cls( type, series, date, notes, products, customer, taxes) @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"): with sqlite3.connect(cls.DB_FILE) as conn: conn.row_factory = sqlite3.Row c = conn.cursor() # PRODUCTS products = () c.execute("""SELECT * FROM products WHERE invoice_id = ? AND invoice_series = ? AND invoice_type = ?""", (id, series, type)) res = c.fetchone() while res is not None: desc = res["description"] un = res["units"] pu = res["price_unit"] vat = res["vat"] products += (Product(desc,un,pu,vat),) res = c.fetchone() # TAXES taxes = () c.execute("""SELECT * FROM taxes WHERE invoice_id = ? AND invoice_series = ? AND invoice_type = ?""", (id, series, type)) res = c.fetchone() while res is not None: taxes += (Tax(res["name"], res["ratio"]) ,) res = c.fetchone() # INVOICE c.execute("""SELECT * FROM invoices WHERE id = ? AND series = ? AND type = ?""", (id, series, type)) res = c.fetchone() return cls( id = id, type = res["type"], series = res["series"], idate = datetime.strptime(res["date"],"%Y-%m-%d").date(), notes = res["notes"], products = products, customer = Customer( res["customer_name"], res["customer_id"], res["customer_address"] ), taxes = taxes) @classmethod def load_many(cls, cond="", vals=(), 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() 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() ) @classmethod def latest_id(cls, series, type, 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() c.execute("""SELECT id FROM invoices WHERE series = ? AND type = ? ORDER BY(id) DESC LIMIT 1""", (series, type)) res = c.fetchall() latest_id = 0 if len(res) != 0: latest_id = int(res[0]["id"]) return latest_id @classmethod def load_by_date(cls, year, quarter=None): dbegin = date(year, 1, 1) dend = date(year+1, 1, 1) if quarter is not None: month = (quarter - 1) * 3 + 1 dbegin = date(year, month, 1) endyear = year endmonth = month + 4 if endmonth > 12: endyear = year+1 endmonth = month dend = date(endyear, endmonth, 1) return cls.load_many( """ WHERE strftime('%s',?) <= strftime('%s',date) AND strftime('%s',date) < strftime('%s',?) """, (dbegin, dend)) def persist(self): conn = sqlite3.connect(Invoice.DB_FILE) if self.id is None: self.id = self.latest_id(self.series, self.type) + 1 c = conn.cursor() c.execute("""INSERT INTO invoices ( type, series, id, id_repr, date, notes, customer_id, customer_name, customer_address ) VALUES (?,?,?,?,?,?,?,?,?)""", ( self.type, self.series, self.id, self.format_id(), self.date.strftime("%Y-%m-%d"), self.notes, self.customer.id, self.customer.name, self.customer.address )) for p in self.products: c.execute("""INSERT INTO products ( invoice_id, invoice_series, invoice_type, description, units, price_unit, vat ) VALUES (?,?,?,?,?,?,?)""",( self.id, self.series, self.type, p.description, p.units, p.price_unit, p.vat )) for x in self.taxes: c.execute("""INSERT INTO taxes ( invoice_id, invoice_series, invoice_type, name, ratio ) VALUES (?,?,?,?,?)""", ( self.id, self.series, self.type, x.name, x.ratio)) conn.commit() conn.close() return self.id 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), "type": self.type, "id": self.format_id(), "date": self.date.strftime("%Y-%m-%d"), "customer_id": self.customer.id, "customer_name": self.customer.name, "customer_address": self.customer.address, "notes": self.notes, } def to_json(self): return json.dumps(self.to_dict()) def to_row(self): row = OrderedDict( type = self.type, id = self.format_id(), date = self.date.strftime("%Y-%m-%d"), customer_id = self.customer.id, customer_name = self.customer.name, ) total_base = 0 total_charged = 0 vats = sorted(Product.VATS) to_base_key = lambda v: "base("+str(v*100)+"%)" to_vat_key = lambda v: "VAT("+str(v*100)+"%)" for vat in vats: ps = filter(lambda p: p.vat == vat, self.products) 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) row[to_vat_key(vat)] = round(charged_vat, Conf.CURRENCY_DECIMAL) total_base += vat_base total_charged += vat_base + charged_vat row["base(TOTAL)"] = round(total_base, Conf.CURRENCY_DECIMAL) to_tax_key = lambda tax: tax.name + "("+ str(tax.ratio*100) +"%)" for i in self.taxes: tax_amount = i.ratio * total_base row[to_tax_key(i)] = round(tax_amount, Conf.CURRENCY_DECIMAL) total_charged += tax_amount row["TOTAL"] = round(total_charged, Conf.CURRENCY_DECIMAL) return row