from invoice import Invoice, Tax, Product, Conf import db from jinja2 import Template 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()) # FIXME all rows don't match and I'm putting them together in weird orders for r in rows: for k in r.keys(): if k not in keys: keys.append(k) import sys wrtr = DictWriter(sys.stdout, keys) wrtr.writeheader() for r in rows: wrtr.writerow(r) @command def render(id, type=None, format=None): # TODO AUTOMATE TEMPLATE SEARCH AND THAT, THIS IS THIS FOR DEV if format is None: raise ValueError("No format specified") invoice = Invoice.load(id, type) if invoice is None: raise ValueError("No invoice found") with open("templates/template." + format, "r") as f: template_text = f.read() template = Template(template_text) print(template.render(invoice=invoice.to_dict())) @command def to_json(id, type=None): invoice = Invoice.load(id, type) 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.add_argument("--format", type=str, help="Invoice type", default="tex") summary_parser.set_defaults(func=render) # json json_parser = subparsers.add_parser("json", aliases=["j"], help="Dump chosen invoice in json format") json_parser.add_argument("id", nargs="?", type=str, help="Invoice identification string") json_parser.add_argument("--type", type=str, help="Invoice type", default="sent") json_parser.set_defaults(func=to_json) # parse args = parser.parse_args() args.func(args)