Coverage for hledger_lots/prompt.py: 28%
156 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 22:41 -0300
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 22:41 -0300
1import subprocess
2from dataclasses import dataclass
3from datetime import datetime
4from typing import List, Optional, Tuple
6import questionary
7from prompt_toolkit.shortcuts import CompleteStyle
9from .avg_info import AllAvgInfo
10from .fifo_info import AllFifoInfo
11from .info import LotsInfo
12from .lib import get_files_comm
15class PromptError(BaseException):
16 def __init__(self, message: str) -> None:
17 self.message = message
18 super().__init__(self.message)
21@dataclass
22class Tradeinfo:
23 date: str
24 quantity: float
25 commodity: str
26 cash_account: str
27 commodity_account: str
28 price: float
29 value: float
32def custom_autocomplete(name: str, choices: List[str]):
33 question = questionary.autocomplete(
34 f"{name} (TAB to autocomplete)",
35 choices=choices,
36 ignore_case=True,
37 match_middle=True,
38 style=questionary.Style([("answer", "fg:#f71b07")]),
39 complete_style=CompleteStyle.MULTI_COLUMN,
40 )
41 return question
44def get_append_file(default_file: str):
45 confirm = questionary.confirm(
46 "Add sale transaction to a journal", default=False, auto_enter=True
47 ).ask()
49 if confirm:
50 file_append: str = questionary.path(
51 "File to append transaction", default=default_file
52 ).ask()
53 comm = ["hledger", "-f", file_append, "check"]
54 subprocess.run(comm, check=True)
55 return file_append
58def select_commodities_text(commodities: List[str]):
59 answer = questionary.select(
60 "Commodity",
61 choices=commodities,
62 use_shortcuts=True,
63 ).ask()
64 return answer
67def ask_commodities_text(commodities: List[str]):
68 answer: str = custom_autocomplete("Commodity", commodities).ask()
69 return answer
72def val_date(date: str):
73 try:
74 datetime.strptime(date, "%Y-%m-%d")
75 return True
76 except ValueError:
77 return "Invalid date format"
80def val_sell_qtty(answer: str, available: float):
81 try:
82 answer_float = float(answer)
83 except ValueError:
84 return "Invalid number"
86 if answer_float <= 0:
87 return "Quantity should be positive"
89 if answer_float > available:
90 return "Quantity should be less than available"
92 return True
95def val_price(answer: str):
96 if answer == "":
97 return True
99 try:
100 answer_float = float(answer)
101 except ValueError:
102 return "Invalid number"
104 if answer_float < 0:
105 return "Price should be positive"
107 return True
110def val_total(answer: str):
111 try:
112 answer_float = float(answer)
113 except ValueError:
114 return "Invalid number"
116 if answer_float < 0:
117 return "Amount should be positive"
119 return True
122class Prompt:
123 def __init__(
124 self,
125 file: Tuple[str, ...],
126 avg_cost: bool,
127 check: bool,
128 no_desc: Optional[str] = None,
129 ) -> None:
130 self.file = file
131 self.check = check
132 self.no_desc = no_desc
133 self.avg_cost = avg_cost
135 self.files_comm = get_files_comm(file)
136 self.infos = self.get_infos()
137 self.commodities = [info["comm"] for info in self.get_infos()]
139 def run_hledger(self, *comm: str):
140 command = ["hledger", *self.files_comm, *comm]
142 if self.no_desc and self.no_desc!= "":
143 command = [*command, f"not:desc:{self.no_desc}"]
145 proc = subprocess.run(command, capture_output=True)
146 if proc.returncode != 0:
147 raise subprocess.SubprocessError(proc.stderr.decode("utf8"))
149 result = proc.stdout.decode("utf8")
150 return result
152 def run_hledger_no_query_desc(self, *comm: str):
153 command = ["hledger", *self.files_comm, *comm]
154 proc = subprocess.run(command, capture_output=True)
155 if proc.returncode != 0:
156 raise subprocess.SubprocessError(proc.stderr.decode("utf8"))
158 result = proc.stdout.decode("utf8")
159 return result
161 def get_infos(self):
162 if self.avg_cost:
163 infos = AllAvgInfo(self.file, self.no_desc or "", self.check)
164 else:
165 infos = AllFifoInfo(self.file, self.no_desc or "", self.check)
167 valid_infos = [info for info in infos.infos if float(info["qtty"]) > 0]
168 return valid_infos
170 def get_last_purchase(self, info: LotsInfo):
171 commodity = info["comm"]
172 reg = self.run_hledger("reg", f"cur:{commodity}", "amt:>0")
173 rows_list = [row for row in reg.split("\n") if row != ""]
175 if len(rows_list) > 0:
176 last_date = rows_list[-1][0:10]
177 return last_date
179 def get_append_file(self):
180 default_file = self.file[0]
182 confirm = questionary.confirm(
183 "Add sale transaction to a journal", default=False, auto_enter=True
184 ).ask()
186 if confirm:
187 file_append: str = questionary.path(
188 "File to append transaction", default=default_file
189 ).ask()
190 comm = ["hledger", "-f", file_append, "check"]
191 subprocess.run(comm, check=True)
192 return file_append
194 def ask_date(self, last_purchase: Optional[str]):
195 last_purchase = last_purchase
197 answer: str = questionary.text(
198 f"Date YYYY-MM-DD",
199 validate=val_date,
200 instruction=f"(Last Purchase: {last_purchase})",
201 ).ask()
202 return answer
204 def ask_sell_qtty(self, info: LotsInfo):
205 available = float(info["qtty"])
207 answer_str: str = questionary.text(
208 f"Quantity (available {available})",
209 validate=lambda answer: val_sell_qtty(answer, available),
210 instruction="",
211 ).ask()
212 return answer_str
214 def ask_price(self, info: LotsInfo):
215 cost_str = info["avg_cost"].replace(",", ".")
216 cost = float(cost_str)
218 answer_str: str = questionary.text(
219 f"Price (avg cost: {cost})",
220 validate=val_price,
221 instruction="Empty to input total amount",
222 ).ask()
223 return answer_str
225 def ask_total(self, qtty: float, info: LotsInfo):
226 available = qtty * float(info["avg_cost"])
227 available_str = f"{available:,.2f}"
229 answer_str: str = questionary.text(
230 f"Total (available {available_str})",
231 validate=val_total,
232 instruction="Amount received",
233 ).ask()
234 return answer_str
236 def ask_cash_account(self):
237 accts_txt = self.run_hledger("accounts")
238 accts = [acct for acct in accts_txt.split("\n") if acct != ""]
239 answer: str = custom_autocomplete("Cash Account", accts).ask()
240 return answer
242 def ask_revenue_account(self):
243 accts_txt = self.run_hledger("accounts")
244 accts = [acct for acct in accts_txt.split("\n") if acct != ""]
245 answer: str = custom_autocomplete("Revenue Account", accts).ask()
246 return answer
248 @property
249 def initial_info(self):
250 cost_method_text = "Average Cost" if self.avg_cost else "Fifo"
251 no_desc_text = self.no_desc or "None"
252 check_text = "Checking" if self.check else "Not Checking"
254 files = [f if f != "-" else "stdin" for f in self.file]
255 files_text = " ".join(files)
257 result = f"""
258Files : {files_text}
259Cost Method : {cost_method_text} - {check_text}
260Remove description : {no_desc_text}
261"""
262 return result