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 # MOVE THIS TO Conf CONFIG_FILE = "" TEMPLATE_DIR = "" 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 directory global TEMPLATE_DIR TEMPLATE_DIR = conf["invoice"].get("templatedir", "") TEMPLATE_DIR = path.expandvars(TEMPLATE_DIR) TEMPLATE_DIR = path.expanduser(TEMPLATE_DIR) if not path.isabs(TEMPLATE_DIR): TEMPLATE_DIR = path.join(basedir, TEMPLATE_DIR) # 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"]) if len(rows) > 0: 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) else: print("No data available for this period") @command def render(id, template=None, type=None, list_templates=None): if list_templates: # List the template directory and exit print("Current templatedir:", TEMPLATE_DIR) print("Available files: ") for i in os.listdir(TEMPLATE_DIR): print("\t", i) return # Find templates and make them work 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(path.join(TEMPLATE_DIR, template) , "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()) @command def duplicate(id, type=None): invoice = Invoice.load(id, type) num = Invoice.from_config( edit( invoice.to_config() )).persist() print(num) 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="Add new invoice") new_parser.set_defaults(func=new_invoice) # Configure fracture configure_parser = subparsers.add_parser("configure", aliases=["c","conf","config"], help="Configure fracture") 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) # Render invoice summary_parser = subparsers.add_parser("render", aliases=["rend", "r"], help="Render chosen invoice in the format defined by a template") summary_parser.add_argument("id", type=str, help="Invoice identification string") summary_parser.add_argument("--type", type=str, help="Invoice type", default="sent") summary_parser.add_argument("--template", "-t", type=str, help="Template to use", default="template.tex") summary_parser.add_argument("--list-templates", "-l", action="store_true", default=False, help="List available templates, ignores other options.") summary_parser.set_defaults(func=render) # Duplicate invoice duplicate_parser= subparsers.add_parser("duplicate", aliases=["dup", "d"], help="Duplicate existing invoice") duplicate_parser.add_argument("id", type=str, help="Invoice identification string") duplicate_parser.add_argument("--type", type=str, help="Invoice type", default="sent") duplicate_parser.set_defaults(func=duplicate) # jsonify json_parser = subparsers.add_parser("json", aliases=["j"], help="Dump chosen invoice in json format") json_parser.add_argument("id", 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)