您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #!/usr/bin/python3
  2. import os, sys, argparse, datetime, subprocess
  3. import invoice.db
  4. import logging
  5. log = logging.getLogger()
  6. class SanityCheckError(Exception):
  7. pass
  8. class Application:
  9. my_company = "my-company"
  10. editor = os.environ.get("EDITOR") or "vim"
  11. viewer = os.environ.get("PAGER") or "less"
  12. tex_program = "pdflatex"
  13. pdf_program = "xdg-open"
  14. def __init__(self, template_path):
  15. self._parse_args()
  16. self.year = self.args.__dict__.pop("year")
  17. self.user_path = os.path.expanduser(self.args.__dict__.pop("user_data"))
  18. self.method = self.args.__dict__.pop("method")
  19. self.data_path = os.path.join(self.user_path, "{year}", "data", "{directory}")
  20. self.tmp_path = os.path.join(self.user_path, "tmp")
  21. self.output_path = os.path.join(self.user_path, "{year}", "output")
  22. self.template_path = template_path
  23. self.db = invoice.db.Database(
  24. year = self.year,
  25. data_path = self.data_path)
  26. def _parse_args(self):
  27. parser = argparse.ArgumentParser(
  28. description = "Pavel Šimerda's invoice CLI application.",
  29. conflict_handler = "resolve")
  30. parser.add_argument("--year", "-y", action="store")
  31. parser.add_argument("--user-data", "-d", action="store")
  32. parser.add_argument("--debug", "-D", action="store_const", dest="log_level", const=logging.DEBUG)
  33. #parser.add_argument("--verbose", "-v", action="store_const", dest="log_level", const=logging.INFO)
  34. #parser.add_argument("--config", "-C", action="store")
  35. parser.set_defaults(
  36. year = datetime.date.today().year,
  37. user_data = "~/.invoice",
  38. log_level = logging.INFO)
  39. subparsers = parser.add_subparsers(title="subcommands",
  40. description="valid subcommands",
  41. help="additional help")
  42. for list_ in "invoices", "companies":
  43. for action in "list", "new", "edit", "show", "delete":
  44. suffix = ''
  45. if list_ == "companies":
  46. suffix = "-companies" if action=="list" else "-company"
  47. method = getattr(self, "do_"+(action+suffix).replace("-", "_"))
  48. subparser = subparsers.add_parser(action+suffix, help=method.__doc__)
  49. if method == self.do_show:
  50. subparser.add_argument("--generate", "-g", action="store_true")
  51. if action == "delete":
  52. subparser.add_argument("--force", "-f", action="store_true")
  53. if action == "new":
  54. subparser.add_argument("name" if suffix else "company_name")
  55. if action in ("show", "edit", "delete"):
  56. subparser.add_argument("selector", nargs="?")
  57. subparser.set_defaults(method=method)
  58. self.args = parser.parse_args()
  59. log.setLevel(self.args.__dict__.pop("log_level"))
  60. log.debug("Arguments: {}".format(self.args))
  61. def run(self):
  62. try:
  63. self.method(**vars(self.args))
  64. except (SanityCheckError) as error:
  65. print("Error: {} Use '--force' to suppress this check.".format(error), file=sys.stderr)
  66. if log.isEnabledFor(logging.DEBUG):
  67. raise
  68. except invoice.db.DatabaseError as error:
  69. print("Error: {}".format(error), file=sys.stderr)
  70. if log.isEnabledFor(logging.DEBUG):
  71. raise
  72. def do_list(self):
  73. """List invoices."""
  74. for item in sorted(self.db.invoices):
  75. print(item)
  76. def do_new(self, company_name):
  77. """Create and edit a new invoice."""
  78. item = self.db.invoices.new(company_name)
  79. self._edit(item._path)
  80. def do_edit(self, selector):
  81. """Edit invoice in external editor.
  82. The external editor is determined by EDITOR environment variable
  83. using 'vim' as the default. Item is edited in-place.
  84. """
  85. if selector:
  86. item = self.db.invoices[selector]
  87. else:
  88. item = self.db.invoices.last()
  89. self._edit(item._path)
  90. def _edit(self, path):
  91. log.debug("Editing file: {}".format(path))
  92. assert os.path.exists(path)
  93. subprocess.call((self.editor, path))
  94. def do_show(self, selector, generate):
  95. """Generate and view a PDF invoice.
  96. This requires Tempita 0.5.
  97. """
  98. import tempita
  99. if selector:
  100. invoice = self.db.invoices[selector]
  101. else:
  102. invoice = self.db.invoices.last()
  103. tmp_path = self.tmp_path.format(year=self.year)
  104. output_path = self.output_path.format(year=self.year)
  105. log.debug("tmp_path={}".format(tmp_path))
  106. tex_template = os.path.join(self.template_path, "invoice.tex")
  107. tex_file = os.path.join(tmp_path, "{}.tex".format(invoice._name))
  108. tmp_pdf_file = os.path.join(tmp_path, "{}.pdf".format(invoice._name))
  109. pdf_file = os.path.join(output_path, "{}.pdf".format(invoice._name))
  110. if generate:
  111. if(not os.path.exists(pdf_file) or
  112. os.path.getmtime(invoice._path) > os.path.getmtime(pdf_file)):
  113. issuer = self.db.companies[self.my_company]
  114. customer = self.db.companies[invoice.company_name]
  115. invoice_data = invoice.data()
  116. issuer_data = issuer.data()
  117. customer_data = customer.data()
  118. log.debug("Invoice: {}".format(invoice_data._data))
  119. log.debug("Issuer: {}".format(issuer_data._data))
  120. log.debug("Customer: {}".format(customer_data._data))
  121. log.debug("Creating TeX invoice...")
  122. self._check_path(self.tmp_path)
  123. result = tempita.Template(open(tex_template).read()).substitute(
  124. invoice=invoice_data, issuer=issuer_data, customer=customer_data)
  125. open(tex_file, "w").write(str(result))
  126. assert(os.path.exists(tex_file))
  127. log.debug("Creating PDF invoice...")
  128. if subprocess.call((self.tex_program, "{}.tex".format(invoice._name)), cwd=tmp_path) != 0:
  129. raise GenerationError("PDF generation failed.")
  130. assert(os.path.exists(tmp_pdf_file))
  131. log.debug("Moving PDF file to the output directory...")
  132. self._check_path(output_path)
  133. os.rename(tmp_pdf_file, pdf_file)
  134. else:
  135. log.info("PDF file is up to date.")
  136. assert(os.path.exists(pdf_file))
  137. log.debug("Running PDF viewer...")
  138. subprocess.call((self.pdf_program, pdf_file))
  139. def _check_path(self, path):
  140. if not os.path.exists(path):
  141. raise LookupError("Directory doesn't exist: {}".format(path))
  142. def do_delete(self, selector, force):
  143. """List invoices."""
  144. if selector:
  145. invoice = self.db.invoices[selector]
  146. else:
  147. invoice = self.db.invoices.last()
  148. if not force:
  149. raise SanityCheckError("It is not recommended to delete invoices.")
  150. invoice.delete()
  151. def do_list_companies(self):
  152. """List companies."""
  153. for item in sorted(self.db.companies):
  154. print(item)
  155. def do_new_company(self, name):
  156. """Create and edit a new company."""
  157. item = self.db.companies.new(name)
  158. self._edit(item._path)
  159. def do_edit_company(self, selector):
  160. """Edit company in external editor.
  161. The external editor is determined by EDITOR environment variable
  162. using 'vim' as the default. Item is edited in-place.
  163. """
  164. item = self.db.companies[selector]
  165. self._edit(item._path)
  166. def do_show_company(self, selector):
  167. """View company in external editor.
  168. The external viewer is determined by PAGER environment variable
  169. using 'less' as the default.
  170. """
  171. item = self.db.companies[selector]
  172. self._show(item._path)
  173. def _show(self, path):
  174. log.debug("Viewing file: {}".format(path))
  175. assert os.path.exists(path)
  176. subprocess.call((self.viewer, path))
  177. def do_delete_company(self, selector, force):
  178. """Delete a company."""
  179. company = self.db.companies[selector]
  180. if not force:
  181. invoices = self.db.invoices.select({"company_name": company._name})
  182. if invoices:
  183. for invoice in invoices:
  184. log.info("Dependent invoice: {}".format(invoice))
  185. raise SanityCheckError("This company is used by some invoices. You should not delete it.")
  186. company.delete()