import sqlite3 from datetime import date, datetime from configparser import ConfigParser import io class Tax: def __init__(self, name="", ratio = 0.0): self.name = name.upper() self.ratio = ratio class Product: 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 self.vat = 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 CURRENCY = None CURRENCY_DECIMAL = 0 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, customers = None, taxes = None): # Initializes to empty state if not set self.set_type(type or tuple(Invoice.TYPES)[0]) self.id = None 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 = customers 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 dump(self): print(self.format_id()) def format_id(self): return Invoice.ID_FORMAT(self.series, self.date, self.id) 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) def persist(self): conn = sqlite3.connect(Invoice.DB_FILE) c = conn.cursor() c.execute("""INSERT INTO invoices ( type, series, date, notes, customer_id, customer_name, customer_address ) VALUES (?,?,?,?,?,?,?)""", ( self.type, self.series, self.date.strftime("%Y-%m-%d"), self.notes, self.customer.id, self.customer.name, self.customer.address )) self.id = c.lastrowid for p in self.products: c.execute("""INSERT INTO products ( invoice_id, description, units, price_unit, vat ) VALUES (?,?,?,?,?)""",( self.id, p.description, p.units, p.price_unit, p.vat )) for x in self.taxes: c.execute("""INSERT INTO taxes ( invoice_id, name, ratio ) VALUES (?,?,?)""", (self.id, x.name, x.ratio)) conn.commit() conn.close() return self.id @classmethod def load(cls, id): 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 = ?", (id,)) 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 = ?", (id,)) 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 = ?", (id,)) res = c.fetchone() return cls( res["type"], res["series"], res["date"], res["notes"], products, Customer( res["customer_name"], res["customer_id"], res["customer_address"] ), taxes) def format(self): # TODO # https://bugs.python.org/issue35111 pass