from invoice import Invoice, Tax, Product, Conf import db from csv import DictWriter import datetime from functools import wraps from argparse import ArgumentParser as ap import tempfile import subprocess import sqlite3 from configparser import ConfigParser import os from os import path CONFIG_FILE = "" def load_config(): basedir = os.environ.get("XDG_CONFIG_HOME", None) or \ path.join(os.environ["HOME"], ".config") basedir = path.join(basedir, "fracture") confile = path.join(basedir, "config") global CONFIG_FILE CONFIG_FILE = confile conf = ConfigParser() conf.read(confile) # VAT-types for k in conf["vat"]: Product.VATS += (float(conf["vat"][k]),) # SERIES for k in conf["series"]: Invoice.SERIES[int(k)] = conf["series"][k] if Invoice.SERIES == {}: raise "Invoice series not configured correctly: no series found" # ID FORMAT FORMAT = conf["invoice"].get("id_format", '"%s/%s/%s" % (series, date.year, id)') def f(series, date, id): return eval(FORMAT, None, {"series": series, "date": date, "id": id}) Invoice.ID_FORMAT = f # CURRENCY Conf.CURRENCY = conf["invoice"].get("currency", "€") Conf.CURRENCY_DECIMAL = conf["invoice"].getint("currency_decimal", 2) # TEMPLATE ? # TODO # Or dump a json or something? templatefile = conf["invoice"]["template"] if not path.isabs(templatefile): templatefile = path.join(confile, templatefile) # INVOICE LEVEL TAXES (like IRPF in Spain) tax = () for k in conf["taxes"]: tax += (Tax(k, conf.getfloat("taxes",k)),) Invoice.DEFAULT_TAXES = tax # DATABASE Invoice.DB_FILE = path.join(basedir, "invoice.db") if not path.exists(Invoice.DB_FILE): db.create(Invoice.DB_FILE) def call_editor(filename): """ Edit filename with $EDITOR (fallback to Vim) """ if not os.path.exists(filename): raise FileNotFoundError("File not found: " + filename) process = subprocess.Popen([os.environ.get("EDITOR", "vim"), filename],) process.wait() def edit(contents): """ Edit temporary file with initial `contents` and return it"s edited content """ with tempfile.NamedTemporaryFile(mode="w", delete=False) as t: t.write(contents) call_editor(t.name) with open(t.name) as t: edited_content = t.read() if os.path.exists(t.name): os.remove(t.name) return edited_content def command(f): @wraps(f) def remove_func_arg(namespace): kwargs = vars(namespace) del kwargs["func"] f(**kwargs) return remove_func_arg @command def edit_config(): # TODO: Generate config file if not created call_editor(CONFIG_FILE) @command def new_invoice(): num = Invoice.from_config( edit( Invoice().to_config() )).persist() # print(num) # edit(Invoice.load(num).to_config()) @command def summarize(xlsx=False, year=None, quarter=None): invoices = Invoice.load_by_date(year, quarter) rows = sorted((map(lambda x: x.to_row(), invoices)), key=lambda x: x["type"]) keys = list(rows[0].keys()) for r in rows: for k in r.keys(): if k not in keys: # TODO: # Inserts all the taxes at the end, but this is not cool # because it needs information about how is the invoice # arranged # Maybe move this to a static function in invoices and call it # summary and make it work with some logic like: create the # keys first and then go setting them in order keys.insert(-1, k) import sys wrtr = DictWriter(sys.stdout, keys) wrtr.writeheader() for r in rows: wrtr.writerow(r) @command def render(id, type=None): invoice = Invoice.load_by_idrepr(id, type) if invoice is not None: print(invoice.to_json()) if __name__ == "__main__": load_config() parser = ap(prog="fracture") parser.set_defaults(func=lambda: parser.print_help()) subparsers = parser.add_subparsers(title= "Subcommands", help="sub-command help") # New Invoice new_parser = subparsers.add_parser("new", aliases=["n"], help="a help") new_parser.set_defaults(func=new_invoice) # Configure fracture configure_parser = subparsers.add_parser("configure", aliases=["c","conf","config"], help="b help") configure_parser.set_defaults(func=edit_config) # Summary summary_parser = subparsers.add_parser("summary", aliases=["s", "sum"], help="Display summary for tax declarations") summary_parser.add_argument("--xlsx", action="store_true", help="Output as xlsx") summary_parser.add_argument("--quarter", type=int, help="Obtain the summary of the quarter") summary_parser.add_argument("--year", type=int, default=datetime.datetime.now().year, help="Obtain the summary of the year") summary_parser.set_defaults(func=summarize) # Dump invoice summary_parser = subparsers.add_parser("render", aliases=["rend", "r"], help="Render chosen invoice in json format") summary_parser.add_argument("id", nargs="?", type=str, help="Invoice identification string") summary_parser.add_argument("--type", type=str, help="Invoice type", default="sent") summary_parser.set_defaults(func=render) # parse args = parser.parse_args() args.func(args)