gchr.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import logging
  2. import os
  3. from dataclasses import dataclass
  4. from datetime import datetime
  5. from pathlib import Path
  6. from typing import Callable
  7. import pandas as pd
  8. from gcstruct.gchr_export import (
  9. ACCOUNT_INFO,
  10. GchrExportConfig,
  11. export_skr51_xml,
  12. header,
  13. )
  14. from gcstruct.gchr_translate import load_translation
  15. @dataclass
  16. class GchrConfig:
  17. first_month_of_financial_year: str
  18. data_dir: str
  19. gcstruct_dir: str
  20. export_dir: str
  21. export_fn = Callable[[GchrExportConfig], None]
  22. class GCHR:
  23. booking_date: datetime
  24. _df_bookings: pd.DataFrame = None
  25. _df_translate: pd.DataFrame = None
  26. df_translate2: pd.DataFrame = None
  27. makes: dict[str, str] = None
  28. sites: dict[str, str] = None
  29. current_year: str
  30. current_month: str
  31. timestamp: str
  32. def __init__(self, base_dir: str) -> None:
  33. self.base_dir = base_dir
  34. os.makedirs(self.base_dir + "/data", exist_ok=True)
  35. os.makedirs(self.base_dir + "/export/temp", exist_ok=True)
  36. os.makedirs(self.base_dir + "/logs", exist_ok=True)
  37. self.account_translation = f"{self.base_dir}/data/Kontenrahmen_uebersetzt.csv"
  38. self.account_bookings = list(Path(self.base_dir).joinpath("data").glob("GuV_Bilanz_Salden*.csv"))
  39. self.first_month_of_financial_year = "10"
  40. self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  41. pd.set_option("display.max_rows", 500)
  42. pd.set_option("display.float_format", lambda x: "%.2f" % x)
  43. def set_bookkeep_period(self, year: str, month: str) -> None:
  44. self.current_year = year
  45. self.current_month = month
  46. self.period = f"{year}-{month}"
  47. prot_file = f"{self.export_info_dir}/protokoll_{self.period}.log"
  48. logging.basicConfig(
  49. filename=prot_file,
  50. filemode="w",
  51. encoding="utf-8",
  52. level=logging.DEBUG,
  53. force=True,
  54. )
  55. @property
  56. def debug_file(self) -> str:
  57. return f"{self.logs_dir}/debug_{self.timestamp}.csv"
  58. @property
  59. def account_ignored(self) -> str:
  60. return f"{self.export_info_dir}/ignoriert_{self.period}.csv"
  61. # self.account_invalid = f"{self.export_info_dir}/ungueltig_{self.period}.csv"
  62. @property
  63. def last_year(self) -> str:
  64. return str(int(self.current_year) - 1)
  65. @property
  66. def last_year2(self) -> str:
  67. return str(int(self.current_year) - 2)
  68. @property
  69. def next_year(self) -> str:
  70. return str(int(self.current_year) + 1)
  71. @property
  72. def bookkeep_filter(self) -> dict[str, str]:
  73. period = [self.current_year + str(i).zfill(2) for i in range(1, 13)]
  74. if self.first_month_of_financial_year != "01":
  75. if self.first_month_of_financial_year > self.current_month:
  76. period = [self.last_year + str(i).zfill(2) for i in range(1, 13)] + period
  77. else:
  78. period = period + [self.next_year + str(i).zfill(2) for i in range(1, 13)]
  79. fm = int(self.first_month_of_financial_year)
  80. period = period[fm - 1 : fm + 12]
  81. period = [self.current_year + "00"] + period
  82. rename_to = ["OpeningBalance"] + ["Period" + str(i).zfill(2) for i in range(1, 13)]
  83. return dict(zip(period, rename_to))
  84. def export_all_periods(self, overwrite=False, today=None) -> None:
  85. periods = GCHR.get_all_periods(today)
  86. for year, month in periods:
  87. filename = self.export_filename_for_period(year, month)
  88. if overwrite or not Path(filename).exists():
  89. os.makedirs(Path(filename).parent.joinpath("info"), exist_ok=True)
  90. self.export_period(year, month)
  91. @staticmethod
  92. def get_all_periods(today=None) -> list[tuple[str, str]]:
  93. dt = datetime.now()
  94. if today is not None:
  95. dt = datetime.fromisoformat(today)
  96. prev = str(dt.year - 1)
  97. periods = [(prev, str(x).zfill(2)) for x in range(dt.month, 13)] + [
  98. (str(dt.year), str(x).zfill(2)) for x in range(1, dt.month)
  99. ]
  100. return periods
  101. def export_period(self, year: str, month: str) -> str:
  102. self.set_bookkeep_period(year, month)
  103. # Kontensalden laden
  104. df_bookings = self.filter_bookings()
  105. all_periods = set(df_bookings["Bookkeep Period"].to_list())
  106. bookkeep_period_date = datetime(int(year), int(month), 28)
  107. if df_bookings.shape[0] == 0 or len(all_periods) <= 1 or self.booking_date < bookkeep_period_date:
  108. logging.error("ABBRUCH!!! Keine Daten vorhanden!")
  109. return False
  110. filter_to = self.current_year + self.current_month
  111. period_no = list(self.bookkeep_filter.keys()).index(filter_to) + 1
  112. logging.info("df_bookings: " + str(df_bookings.shape))
  113. # Join auf Übersetzung
  114. df_combined = df_bookings.merge(self._df_translate, how="inner", on="Konto_Nr_Händler")
  115. logging.info(f"df_combined: {df_combined.shape}")
  116. df_pivot = df_combined.pivot_table(
  117. index=["Konto_Nr_SKR51"],
  118. columns=["period"],
  119. values="amount",
  120. aggfunc="sum",
  121. margins=True,
  122. margins_name="CumulatedYear",
  123. )
  124. df_pivot.drop(index="CumulatedYear", inplace=True)
  125. logging.info("df_pivot: " + str(df_pivot.shape))
  126. df = df_pivot.merge(self.df_translate2, how="inner", on="Konto_Nr_SKR51")
  127. makes_used = {}
  128. for m in sorted(list(set(df["Marke"].to_list()))):
  129. if m not in self.makes:
  130. continue
  131. makes_used[m] = self.makes[m]
  132. sites_used = {}
  133. for s in sorted(list(set((df["Marke"] + "-" + df["Standort"]).to_list()))):
  134. if s not in self.sites:
  135. continue
  136. sites_used[s] = self.sites[s]
  137. from_label = ["Marke", "Standort", "Konto_Nr", "Kostenstelle", "Absatzkanal", "Kostenträger", "KRM"]
  138. to_label = ["Make", "Site", "Account", "Origin", "SalesChannel", "CostCarrier", "CostAccountingString"]
  139. col_dict = dict(zip(from_label, to_label))
  140. df = df.rename(columns=col_dict)
  141. export_csv = self.export_filename[:-4] + ".csv"
  142. df.to_csv(export_csv, decimal=",", sep=";", encoding="latin-1", index=False)
  143. df = df[df["IsNumeric"] != False].groupby(ACCOUNT_INFO, as_index=False).aggregate("sum")
  144. # Infos ergänzen
  145. df["Decimals"] = 2
  146. # df.sort_values(by=["Konto_Nr_SKR51"], inplace=True)
  147. logging.info(df.shape)
  148. main_sites = [self.sites[s] for s in sites_used if s in self.sites and self.sites[s] != "0000"]
  149. for i, main_site in enumerate(main_sites):
  150. filename = self.export_filename
  151. if i > 0:
  152. filename = f"{filename[:-4]}_{main_site}.xml"
  153. export_cfg = GchrExportConfig(
  154. main_site,
  155. year,
  156. month,
  157. makes_used,
  158. sites_used,
  159. self.first_month_of_financial_year,
  160. period_no,
  161. self.bookkeep_filter,
  162. filename,
  163. df.to_dict(orient="records"),
  164. )
  165. export_cfg.header = header(export_cfg)
  166. export_skr51_xml(export_cfg)
  167. # Join auf Übersetzung - nicht zugeordnet
  168. df_ignored = df_bookings.merge(self.df_translate, how="left", on="Konto_Nr_Händler")
  169. df_ignored = df_ignored[df_ignored["Konto_Nr_SKR51"].isna()]
  170. if not df_ignored.empty:
  171. df_ignored = df_ignored.pivot_table(
  172. index=["Konto_Nr_Händler"],
  173. columns=["period"],
  174. values="amount",
  175. aggfunc="sum",
  176. margins=True,
  177. margins_name="CumulatedYear",
  178. )
  179. df_ignored.to_csv(self.account_ignored, decimal=",", sep=";", encoding="latin-1")
  180. return self.export_filename
  181. @property
  182. def df_translate(self) -> pd.DataFrame:
  183. if self._df_translate is None:
  184. self.makes, self.sites, self._df_translate, self.df_translate2 = load_translation(
  185. self.account_translation, self.debug_file, self.export_invalid_filename
  186. )
  187. return self._df_translate
  188. def load_bookings_from_file(self) -> None:
  189. df_list: list[pd.DataFrame] = []
  190. timestamps: list[float] = []
  191. for csv_file in self.account_bookings:
  192. df_list.append(
  193. pd.read_csv(
  194. csv_file,
  195. decimal=",",
  196. sep=";",
  197. encoding="latin-1",
  198. converters={0: str, 1: str},
  199. )
  200. )
  201. timestamps.append(Path(csv_file).stat().st_mtime)
  202. self.booking_date = datetime.fromtimestamp(max(timestamps))
  203. df = pd.concat(df_list)
  204. df["amount"] = (df["Debit Amount"] + df["Credit Amount"]).round(2)
  205. return df
  206. @property
  207. def df_bookings(self) -> pd.DataFrame:
  208. if self._df_bookings is None:
  209. self._df_bookings = self.load_bookings_from_file()
  210. return self._df_bookings
  211. def filter_bookings(self) -> pd.DataFrame:
  212. # Kontensalden auf gegebenen Monat filtern
  213. filter_from = self.current_year + self.first_month_of_financial_year
  214. filter_prev = self.last_year + self.first_month_of_financial_year
  215. if self.first_month_of_financial_year > self.current_month:
  216. filter_from = self.last_year + self.first_month_of_financial_year
  217. filter_prev = self.last_year2 + self.first_month_of_financial_year
  218. filter_to = self.current_year + self.current_month
  219. filter_opening = self.current_year + "00"
  220. filter_prev_opening = self.last_year + "00"
  221. prev_year_closed = True
  222. df_opening_balance = self.df_bookings[(self.df_bookings["Bookkeep Period"] == filter_opening)]
  223. if df_opening_balance.shape[0] == 0:
  224. df_opening_balance = self.df_bookings[
  225. (self.df_bookings["Bookkeep Period"] == filter_prev_opening)
  226. | (
  227. (self.df_bookings["Bookkeep Period"] >= filter_prev)
  228. & (self.df_bookings["Bookkeep Period"] < filter_from)
  229. )
  230. ].copy()
  231. df_opening_balance["Bookkeep Period"] = filter_opening
  232. prev_year_closed = False
  233. df_opening_balance = df_opening_balance[(df_opening_balance["Konto_Nr_Händler"].str.contains(r"-[013]\d\d+-"))]
  234. opening_balance = df_opening_balance["amount"].aggregate("sum").round(2)
  235. logging.info("Gewinn/Verlustvortrag")
  236. logging.info(opening_balance)
  237. if not prev_year_closed:
  238. row = {
  239. "Konto_Nr_Händler": "01-01-0861-00-00-00",
  240. "Bookkeep Period": filter_opening,
  241. "Debit Amount": opening_balance * -1,
  242. "Credit Amount": 0,
  243. "Debit Quantity": 0,
  244. "Credit Quantity": 0,
  245. "amount": opening_balance * -1,
  246. }
  247. df_opening_balance = pd.concat([df_opening_balance, pd.DataFrame.from_records([row])])
  248. df_filtered = self.df_bookings[
  249. (self.df_bookings["Bookkeep Period"] >= filter_from) & (self.df_bookings["Bookkeep Period"] <= filter_to)
  250. ]
  251. # Buchungen kopieren und als Statistikkonten anhängen
  252. df_stats = df_filtered.copy()
  253. # df_stats = df_stats[df_stats['Konto_Nr_Händler'].str.match(r'-[24578]\d\d\d-')]
  254. df_stats["Konto_Nr_Händler"] = df_stats["Konto_Nr_Händler"].str.replace(r"-(\d\d\d+)-", r"-\1_STK-", regex=True)
  255. df_stats["amount"] = (df_filtered["Debit Quantity"] + df_filtered["Credit Quantity"]).round(2)
  256. df_combined = pd.concat([df_opening_balance, df_filtered, df_stats])
  257. # Spalten konvertieren
  258. df_combined["period"] = df_combined["Bookkeep Period"].apply(lambda x: self.bookkeep_filter[x])
  259. return df_combined[df_combined["amount"] != 0.00]
  260. @property
  261. def export_filename(self) -> str:
  262. return self.export_filename_for_period(self.current_year, self.current_month)
  263. @property
  264. def export_info_dir(self) -> str:
  265. return f"{self.base_dir}/Export/{self.current_year}/info/"
  266. @property
  267. def logs_dir(self) -> str:
  268. return f"{self.base_dir}/Logs/"
  269. @property
  270. def export_invalid_filename(self) -> str:
  271. return f"{self.base_dir}/Export/ungueltig.csv"
  272. def export_filename_for_period(self, year: str, month: str) -> str:
  273. return f"{self.base_dir}/Export/{year}/export_{year}-{month}.xml"
  274. def gchr_local() -> None:
  275. base_dir = os.getcwd() + "/../GCHR2_Testdaten/Kunden"
  276. for path in Path(base_dir).glob("*"):
  277. if path.is_dir():
  278. print(path.name)
  279. gchr_export(str(path))
  280. def gchr_export(base_dir: str) -> None:
  281. gchr = GCHR(base_dir)
  282. # gchr.export_all_periods(overwrite=True, today="2022-08-01")
  283. gchr.export_all_periods()