gchr.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. import csv
  2. import logging
  3. import os
  4. import xml.etree.ElementTree as ET
  5. from dataclasses import dataclass
  6. from datetime import datetime
  7. from pathlib import Path
  8. from typing import Callable
  9. import numpy as np
  10. import pandas as pd
  11. from gcstruct.gchr_export import (
  12. ACCOUNT_INFO,
  13. GchrExportConfig,
  14. export_skr51_xml,
  15. header,
  16. )
  17. TRANSLATE = [
  18. "Konto_Nr_Händler",
  19. "Konto_Nr_SKR51",
  20. "Marke",
  21. "Standort",
  22. "Konto_Nr",
  23. "Kostenstelle",
  24. "Absatzkanal",
  25. "Kostenträger",
  26. "Kontoart",
  27. "Konto_1",
  28. "KRM",
  29. "IsNumeric",
  30. ]
  31. @dataclass
  32. class GchrConfig:
  33. first_month_of_financial_year: str
  34. data_path: str
  35. gcstruct_path: str
  36. export_path: str
  37. export_fn = Callable[[GchrExportConfig], None]
  38. class GCHR:
  39. booking_date: datetime
  40. _df_bookings: pd.DataFrame = None
  41. _df_translate: pd.DataFrame = None
  42. df_translate2: pd.DataFrame = None
  43. makes: dict[str, str] = None
  44. sites: dict[str, str] = None
  45. current_year: str
  46. current_month: str
  47. timestamp: str
  48. def __init__(self, base_dir: str) -> None:
  49. self.base_dir = base_dir
  50. os.makedirs(self.base_dir + "/data", exist_ok=True)
  51. os.makedirs(self.base_dir + "/export/temp", exist_ok=True)
  52. os.makedirs(self.base_dir + "/logs", exist_ok=True)
  53. self.account_translation = f"{self.base_dir}/data/Kontenrahmen_uebersetzt.csv"
  54. self.account_bookings = list(Path(self.base_dir).joinpath("data").glob("GuV_Bilanz_Salden*.csv"))
  55. self.first_month_of_financial_year = "10"
  56. self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  57. pd.set_option("display.max_rows", 500)
  58. pd.set_option("display.float_format", lambda x: "%.2f" % x)
  59. def set_bookkeep_period(self, year: str, month: str) -> None:
  60. self.current_year = year
  61. self.current_month = month
  62. self.period = f"{year}-{month}"
  63. prot_file = f"{self.export_info_dir}/protokoll_{self.period}.log"
  64. logging.basicConfig(
  65. filename=prot_file,
  66. filemode="w",
  67. encoding="utf-8",
  68. level=logging.DEBUG,
  69. force=True,
  70. )
  71. @property
  72. def debug_file(self) -> str:
  73. return f"{self.logs_dir}/debug_{self.timestamp}.csv"
  74. @property
  75. def account_ignored(self) -> str:
  76. return f"{self.export_info_dir}/ignoriert_{self.period}.csv"
  77. # self.account_invalid = f"{self.export_info_dir}/ungueltig_{self.period}.csv"
  78. @property
  79. def last_year(self) -> str:
  80. return str(int(self.current_year) - 1)
  81. @property
  82. def last_year2(self) -> str:
  83. return str(int(self.current_year) - 2)
  84. @property
  85. def next_year(self) -> str:
  86. return str(int(self.current_year) + 1)
  87. @property
  88. def bookkeep_filter(self) -> dict[str, str]:
  89. period = [self.current_year + str(i).zfill(2) for i in range(1, 13)]
  90. if self.first_month_of_financial_year != "01":
  91. if self.first_month_of_financial_year > self.current_month:
  92. period = [self.last_year + str(i).zfill(2) for i in range(1, 13)] + period
  93. else:
  94. period = period + [self.next_year + str(i).zfill(2) for i in range(1, 13)]
  95. fm = int(self.first_month_of_financial_year)
  96. period = period[fm - 1 : fm + 12]
  97. period = [self.current_year + "00"] + period
  98. rename_to = ["OpeningBalance"] + ["Period" + str(i).zfill(2) for i in range(1, 13)]
  99. return dict(zip(period, rename_to))
  100. @staticmethod
  101. def extract_acct_info(df: pd.DataFrame) -> pd.DataFrame:
  102. acct_info = [
  103. "Marke",
  104. "Standort",
  105. "Konto_Nr",
  106. "Kostenstelle",
  107. "Absatzkanal",
  108. "Kostenträger",
  109. ]
  110. df["HasFiveDashes"] = df["Konto_Nr_SKR51"].str.count("-") == 5
  111. df["Invalid"] = "XX-XX-XXXX-XX-XX-XX"
  112. df["Konto_Nr_SKR51"] = np.where(
  113. df["HasFiveDashes"],
  114. df["Konto_Nr_SKR51"],
  115. df["Invalid"],
  116. )
  117. df[acct_info] = df["Konto_Nr_SKR51"].str.split(pat="-", n=6, expand=True)
  118. return df
  119. def export_all_periods(self, overwrite=False, today=None) -> None:
  120. dt = datetime.now()
  121. if today is not None:
  122. dt = datetime.fromisoformat(today)
  123. prev = str(dt.year - 1)
  124. periods = [(prev, str(x).zfill(2)) for x in range(dt.month, 13)] + [
  125. (str(dt.year), str(x).zfill(2)) for x in range(1, dt.month)
  126. ]
  127. for year, month in periods:
  128. filename = self.export_filename_for_period(year, month)
  129. if overwrite or not Path(filename).exists():
  130. os.makedirs(Path(filename).parent.joinpath("info"), exist_ok=True)
  131. self.export_period(year, month)
  132. def export_period(self, year: str, month: str) -> str:
  133. self.set_bookkeep_period(year, month)
  134. # Kontensalden laden
  135. df_bookings = self.filter_bookings()
  136. all_periods = set(df_bookings["Bookkeep Period"].to_list())
  137. bookkeep_period_date = datetime(int(year), int(month), 28)
  138. if df_bookings.shape[0] == 0 or len(all_periods) <= 1 or self.booking_date < bookkeep_period_date:
  139. logging.error("ABBRUCH!!! Keine Daten vorhanden!")
  140. return False
  141. filter_to = self.current_year + self.current_month
  142. period_no = list(self.bookkeep_filter.keys()).index(filter_to) + 1
  143. logging.info("df_bookings: " + str(df_bookings.shape))
  144. # Join auf Übersetzung
  145. df_combined = df_bookings.merge(self._df_translate, how="inner", on="Konto_Nr_Händler")
  146. logging.info(f"df_combined: {df_combined.shape}")
  147. df_pivot = df_combined.pivot_table(
  148. index=["Konto_Nr_SKR51"],
  149. columns=["period"],
  150. values="amount",
  151. aggfunc="sum",
  152. margins=True,
  153. margins_name="CumulatedYear",
  154. )
  155. df_pivot.drop(index="CumulatedYear", inplace=True)
  156. logging.info("df_pivot: " + str(df_pivot.shape))
  157. df = df_pivot.merge(self.df_translate2, how="inner", on="Konto_Nr_SKR51")
  158. makes_used = {}
  159. for m in sorted(list(set(df["Marke"].to_list()))):
  160. if m not in self.makes:
  161. continue
  162. makes_used[m] = self.makes[m]
  163. sites_used = {}
  164. for s in sorted(list(set((df["Marke"] + "-" + df["Standort"]).to_list()))):
  165. if s not in self.sites:
  166. continue
  167. sites_used[s] = self.sites[s]
  168. from_label = ["Marke", "Standort", "Konto_Nr", "Kostenstelle", "Absatzkanal", "Kostenträger", "KRM"]
  169. to_label = ["Make", "Site", "Account", "Origin", "SalesChannel", "CostCarrier", "CostAccountingString"]
  170. col_dict = dict(zip(from_label, to_label))
  171. df = df.rename(columns=col_dict)
  172. export_csv = self.export_filename[:-4] + ".csv"
  173. df.to_csv(export_csv, decimal=",", sep=";", encoding="latin-1", index=False)
  174. df = df[df["IsNumeric"] != False].groupby(ACCOUNT_INFO, as_index=False).aggregate("sum")
  175. # Infos ergänzen
  176. df["Decimals"] = 2
  177. # df.sort_values(by=["Konto_Nr_SKR51"], inplace=True)
  178. logging.info(df.shape)
  179. main_sites = [self.sites[s] for s in sites_used if s in self.sites and self.sites[s] != "0000"]
  180. for i, main_site in enumerate(main_sites):
  181. filename = self.export_filename
  182. if i > 0:
  183. filename = f"{filename[:-4]}_{main_site}.xml"
  184. export_cfg = GchrExportConfig(
  185. main_site,
  186. year,
  187. month,
  188. makes_used,
  189. sites_used,
  190. self.first_month_of_financial_year,
  191. period_no,
  192. self.bookkeep_filter,
  193. filename,
  194. df.to_dict(orient="records"),
  195. )
  196. export_cfg.header = header(export_cfg)
  197. export_skr51_xml(export_cfg)
  198. # Join auf Übersetzung - nicht zugeordnet
  199. df_ignored = df_bookings.merge(self.df_translate, how="left", on="Konto_Nr_Händler")
  200. df_ignored = df_ignored[df_ignored["Konto_Nr_SKR51"].isna()]
  201. if not df_ignored.empty:
  202. df_ignored = df_ignored.pivot_table(
  203. index=["Konto_Nr_Händler"],
  204. columns=["period"],
  205. values="amount",
  206. aggfunc="sum",
  207. margins=True,
  208. margins_name="CumulatedYear",
  209. )
  210. df_ignored.to_csv(self.account_ignored, decimal=",", sep=";", encoding="latin-1")
  211. return self.export_filename
  212. @property
  213. def df_translate(self) -> pd.DataFrame:
  214. if self._df_translate is None:
  215. self.makes, self.sites, self._df_translate, self.df_translate2 = GCHR.load_translation(
  216. self.account_translation, self.debug_file, self.export_invalid_filename
  217. )
  218. return self._df_translate
  219. @staticmethod
  220. def load_translation(
  221. account_translation: str, debug_file: str, export_invalid_filename: str
  222. ) -> tuple[dict, dict, pd.DataFrame, pd.DataFrame]:
  223. df_translate_import = pd.read_csv(
  224. account_translation,
  225. decimal=",",
  226. sep=";",
  227. encoding="latin-1",
  228. converters={i: str for i in range(0, 200)},
  229. ).reset_index()
  230. makes = GCHR.get_makes_from_translation(df_translate_import)
  231. sites = GCHR.get_sites_from_translation(df_translate_import)
  232. df_prepared = GCHR.prepare_translation(df_translate_import)
  233. df_translate = GCHR.special_translation(df_prepared, makes, sites, debug_file, export_invalid_filename)
  234. df_translate2 = (
  235. df_translate.copy().drop(columns=["Konto_Nr_Händler"]).drop_duplicates().set_index("Konto_Nr_SKR51")
  236. )
  237. return (makes, sites, df_translate, df_translate2)
  238. @staticmethod
  239. def get_makes_from_translation(df_translate_import: pd.DataFrame) -> dict[str, str]:
  240. df_makes = df_translate_import[["Marke", "Marke_HBV"]].copy().drop_duplicates()
  241. df_makes = df_makes[df_makes["Marke_HBV"] != "0000"]
  242. makes = dict([(e["Marke"], e["Marke_HBV"]) for e in df_makes.to_dict(orient="records")])
  243. makes["99"] = "0000"
  244. return makes
  245. @staticmethod
  246. def get_sites_from_translation(df_translate_import: pd.DataFrame) -> dict[str, str]:
  247. df_sites = df_translate_import[["Marke", "Standort", "Standort_HBV"]].copy().drop_duplicates()
  248. df_sites["Standort_HBV"] = np.where(df_sites["Standort_HBV"].str.len() != 6, "0000", df_sites["Standort_HBV"])
  249. sites = dict(
  250. [(e["Marke"] + "-" + e["Standort"], e["Standort_HBV"]) for e in df_sites.to_dict(orient="records")]
  251. )
  252. return sites
  253. @staticmethod
  254. def prepare_translation(df_translate_import: pd.DataFrame) -> pd.DataFrame:
  255. df = df_translate_import[
  256. [
  257. "Konto_Nr_Händler",
  258. "Konto_Nr_SKR51",
  259. ]
  260. ].drop_duplicates()
  261. logging.info(df.shape)
  262. row = {
  263. "Konto_Nr_Händler": "01-01-0861-00-00-00",
  264. "Konto_Nr_SKR51": "01-01-0861-00-00-00",
  265. }
  266. df = pd.concat([df, pd.DataFrame.from_records([row])])
  267. df.set_index("Konto_Nr_Händler")
  268. return df
  269. @staticmethod
  270. def special_translation(
  271. df: pd.DataFrame, makes: dict[str, str], sites: dict[str, str], debug_file: str, export_invalid_filename: str
  272. ) -> pd.DataFrame:
  273. df["Konto_Nr_Händler"] = df["Konto_Nr_Händler"].str.upper()
  274. df["Konto_Nr_SKR51"] = df["Konto_Nr_SKR51"].str.upper()
  275. df = GCHR.extract_acct_info(df)
  276. df["Konto_Nr"] = df["Konto_Nr"].str.upper()
  277. logging.info(df.shape)
  278. logging.info(df.columns)
  279. logging.info(df.head())
  280. logging.info("df: " + str(df.shape))
  281. df["Bilanz"] = df["Konto_Nr"].str.match(r"^[013]")
  282. df["Kontoart"] = np.where(df["Bilanz"], "1", "2")
  283. df["Kontoart"] = np.where(df["Konto_Nr"].str.contains("_STK"), "3", df["Kontoart"])
  284. df["Kontoart"] = np.where(df["Konto_Nr"].str.match(r"^[9]"), "3", df["Kontoart"])
  285. df["Konto_1"] = df["Konto_Nr"].str.slice(0, 1)
  286. # fehlende Marken- und Standortzuordnung
  287. df["Marke"] = np.where(df["Marke"].isin(makes.keys()), df["Marke"], "99")
  288. df["Marke_Standort"] = df["Marke"] + "-" + df["Standort"]
  289. df["Standort"] = np.where(df["Marke_Standort"].isin(sites.keys()), df["Standort"], "01")
  290. df_debug = df.drop(columns=["Bilanz"])
  291. logging.info(df_debug.groupby(["Kontoart"]).aggregate("sum"))
  292. logging.info(df_debug.groupby(["Kontoart", "Konto_1"]).aggregate("sum"))
  293. logging.info(df_debug.groupby(["Konto_Nr"]).aggregate("sum"))
  294. df_debug.groupby(["Konto_Nr"]).aggregate("sum").to_csv(debug_file, decimal=",", sep=";", encoding="latin-1")
  295. # Bereinigung GW-Kostenträger
  296. df["NW_Verkauf_1"] = (df["Konto_Nr"].str.match(r"^[78]0")) & (df["Kostenstelle"].str.match(r"^[^1]\d"))
  297. df["Kostenstelle"] = np.where(df["NW_Verkauf_1"] == True, "11", df["Kostenstelle"])
  298. df["Konto_7010"] = df["Konto_Nr"].str.match(r"^[78]01[01]")
  299. df["Kostenstelle"] = np.where(df["Konto_7010"] == True, "14", df["Kostenstelle"])
  300. df["GW_Verkauf_2"] = (df["Konto_Nr"].str.match(r"^[78]1")) & (df["Kostenstelle"].str.match(r"^[^2]\d"))
  301. df["Kostenstelle"] = np.where(df["GW_Verkauf_2"] == True, "21", df["Kostenstelle"])
  302. df["GW_Verkauf_3"] = (df["Konto_Nr"].str.match(r"^[78]3")) & (df["Kostenstelle"].str.match(r"^[^3]\d"))
  303. df["Kostenstelle"] = np.where(df["GW_Verkauf_3"] == True, "31", df["Kostenstelle"])
  304. df["GW_Verkauf_4"] = (df["Konto_Nr"].str.match(r"^[78]4")) & (df["Kostenstelle"].str.match(r"^[^4]\d"))
  305. df["Kostenstelle"] = np.where(df["GW_Verkauf_4"] == True, "41", df["Kostenstelle"])
  306. df["GW_Verkauf_x420"] = df["Konto_Nr"].str.match(r"^[78]420")
  307. df["Kostenstelle"] = np.where(df["GW_Verkauf_x420"] == True, "42", df["Kostenstelle"])
  308. df["GW_Verkauf_5"] = (df["Konto_Nr"].str.match(r"^[78]5")) & (df["Kostenstelle"].str.match(r"^[^5]\d"))
  309. df["Kostenstelle"] = np.where(df["GW_Verkauf_5"] == True, "51", df["Kostenstelle"])
  310. df["GW_Verkauf_50"] = (df["Konto_Nr"].str.match(r"^[78]")) & (df["Kostenstelle"].str.match(r"^2"))
  311. df["Kostenträger"] = np.where(df["GW_Verkauf_50"] == True, "52", df["Kostenträger"])
  312. df["Kostenträger"] = np.where(
  313. (df["GW_Verkauf_50"] == True) & (df["Marke"] == "01"),
  314. "50",
  315. df["Kostenträger"],
  316. )
  317. df["NW_Verkauf_00"] = (
  318. (df["Konto_Nr"].str.match(r"^[78]2"))
  319. & (df["Kostenstelle"].str.match(r"^1"))
  320. & (df["Kostenträger"].str.match(r"^[^01234]"))
  321. )
  322. df["Kostenträger"] = np.where(df["NW_Verkauf_00"] == True, "00", df["Kostenträger"])
  323. df["GW_Stk_50"] = (df["Konto_Nr"].str.match(r"^9130")) & (df["Kostenstelle"].str.match(r"^2"))
  324. df["Kostenträger"] = np.where(df["GW_Stk_50"] == True, "52", df["Kostenträger"])
  325. df["Kostenträger"] = np.where((df["GW_Stk_50"] == True) & (df["Marke"] == "01"), "50", df["Kostenträger"])
  326. df["Kostenträger"] = np.where(df["Bilanz"] == True, "00", df["Kostenträger"])
  327. df["Konto_5er"] = (df["Konto_Nr"].str.match("^5")) | (df["Konto_Nr"].str.match("^9143"))
  328. df["Absatzkanal"] = np.where(df["Konto_5er"] == True, "99", df["Absatzkanal"])
  329. df["Konto_5005"] = (df["Konto_Nr"].str.match("^5005")) & (df["Kostenstelle"].str.match(r"^[^12]"))
  330. df["Kostenstelle"] = np.where(df["Konto_5005"] == True, "20", df["Kostenstelle"])
  331. df["Kostenträger"] = np.where(df["Konto_5005"] == True, "50", df["Kostenträger"])
  332. df["Konto_5007"] = (df["Konto_Nr"].str.match("^5007")) & (df["Kostenstelle"].str.match(r"^([^4]|42)"))
  333. df["Kostenstelle"] = np.where(df["Konto_5007"] == True, "41", df["Kostenstelle"])
  334. df["Kostenträger"] = np.where(df["Konto_5007"] == True, "70", df["Kostenträger"])
  335. df["Konto_914er"] = (df["Konto_Nr"].str.match("^914[34]")) & (df["Kostenträger"].str.match(r"^[^7]"))
  336. df["Kostenträger"] = np.where(df["Konto_914er"] == True, "70", df["Kostenträger"])
  337. df["Teile_30_60"] = (
  338. (df["Konto_Nr"].str.match(r"^[578]"))
  339. & (df["Kostenstelle"].str.match(r"^[3]"))
  340. & (df["Kostenträger"].str.match(r"^[^6]"))
  341. )
  342. df["Kostenträger"] = np.where(df["Teile_30_60"] == True, "60", df["Kostenträger"])
  343. df["Service_40_70"] = (
  344. (df["Konto_Nr"].str.match(r"^[578]"))
  345. & (df["Kostenstelle"].str.match(r"^[4]"))
  346. & (df["Kostenträger"].str.match(r"^[^7]"))
  347. )
  348. df["Kostenträger"] = np.where(df["Service_40_70"] == True, "70", df["Kostenträger"])
  349. df["KRM"] = df["Marke"] + df["Standort"] + df["Kostenstelle"] + df["Absatzkanal"] + df["Kostenträger"]
  350. df["Konto_Nr_SKR51"] = (
  351. (df["Marke"] + "-" + df["Standort"] + "-" + df["Konto_Nr"])
  352. + "-"
  353. + (df["Kostenstelle"] + "-" + df["Absatzkanal"] + "-" + df["Kostenträger"])
  354. )
  355. df["IsNumeric"] = (
  356. (df["KRM"].str.isdigit())
  357. & (df["Konto_Nr"].str.isdigit())
  358. & (df["Konto_Nr"].str.len() == 4)
  359. # & (df["Konto_Nr_SKR51"].str.len() == 19)
  360. )
  361. df_invalid = df[df["IsNumeric"] == False]
  362. df_invalid.to_csv(export_invalid_filename, decimal=",", sep=";", encoding="latin-1", index=False)
  363. return df[df["IsNumeric"] == True][TRANSLATE]
  364. def load_bookings_from_file(self) -> None:
  365. df_list: list[pd.DataFrame] = []
  366. timestamps: list[float] = []
  367. for csv_file in self.account_bookings:
  368. df_list.append(
  369. pd.read_csv(
  370. csv_file,
  371. decimal=",",
  372. sep=";",
  373. encoding="latin-1",
  374. converters={0: str, 1: str},
  375. )
  376. )
  377. timestamps.append(Path(csv_file).stat().st_mtime)
  378. self.booking_date = datetime.fromtimestamp(max(timestamps))
  379. df = pd.concat(df_list)
  380. df["amount"] = (df["Debit Amount"] + df["Credit Amount"]).round(2)
  381. return df
  382. @property
  383. def df_bookings(self) -> pd.DataFrame:
  384. if self._df_bookings is None:
  385. self._df_bookings = self.load_bookings_from_file()
  386. return self._df_bookings
  387. def filter_bookings(self) -> pd.DataFrame:
  388. # Kontensalden auf gegebenen Monat filtern
  389. filter_from = self.current_year + self.first_month_of_financial_year
  390. filter_prev = self.last_year + self.first_month_of_financial_year
  391. if self.first_month_of_financial_year > self.current_month:
  392. filter_from = self.last_year + self.first_month_of_financial_year
  393. filter_prev = self.last_year2 + self.first_month_of_financial_year
  394. filter_to = self.current_year + self.current_month
  395. filter_opening = self.current_year + "00"
  396. filter_prev_opening = self.last_year + "00"
  397. prev_year_closed = True
  398. df_opening_balance = self.df_bookings[(self.df_bookings["Bookkeep Period"] == filter_opening)]
  399. if df_opening_balance.shape[0] == 0:
  400. df_opening_balance = self.df_bookings[
  401. (self.df_bookings["Bookkeep Period"] == filter_prev_opening)
  402. | (
  403. (self.df_bookings["Bookkeep Period"] >= filter_prev)
  404. & (self.df_bookings["Bookkeep Period"] < filter_from)
  405. )
  406. ].copy()
  407. df_opening_balance["Bookkeep Period"] = filter_opening
  408. prev_year_closed = False
  409. df_opening_balance = df_opening_balance[(df_opening_balance["Konto_Nr_Händler"].str.contains(r"-[013]\d\d+-"))]
  410. opening_balance = df_opening_balance["amount"].aggregate("sum").round(2)
  411. logging.info("Gewinn/Verlustvortrag")
  412. logging.info(opening_balance)
  413. if not prev_year_closed:
  414. row = {
  415. "Konto_Nr_Händler": "01-01-0861-00-00-00",
  416. "Bookkeep Period": filter_opening,
  417. "Debit Amount": opening_balance * -1,
  418. "Credit Amount": 0,
  419. "Debit Quantity": 0,
  420. "Credit Quantity": 0,
  421. "amount": opening_balance * -1,
  422. }
  423. df_opening_balance = pd.concat([df_opening_balance, pd.DataFrame.from_records([row])])
  424. df_filtered = self.df_bookings[
  425. (self.df_bookings["Bookkeep Period"] >= filter_from) & (self.df_bookings["Bookkeep Period"] <= filter_to)
  426. ]
  427. # Buchungen kopieren und als Statistikkonten anhängen
  428. df_stats = df_filtered.copy()
  429. # df_stats = df_stats[df_stats['Konto_Nr_Händler'].str.match(r'-[24578]\d\d\d-')]
  430. df_stats["Konto_Nr_Händler"] = df_stats["Konto_Nr_Händler"].str.replace(r"-(\d\d\d+)-", r"-\1_STK-", regex=True)
  431. df_stats["amount"] = (df_filtered["Debit Quantity"] + df_filtered["Credit Quantity"]).round(2)
  432. df_combined = pd.concat([df_opening_balance, df_filtered, df_stats])
  433. # Spalten konvertieren
  434. df_combined["period"] = df_combined["Bookkeep Period"].apply(lambda x: self.bookkeep_filter[x])
  435. return df_combined[df_combined["amount"] != 0.00]
  436. @property
  437. def export_filename(self) -> str:
  438. return self.export_filename_for_period(self.current_year, self.current_month)
  439. @property
  440. def export_info_dir(self) -> str:
  441. return f"{self.base_dir}/Export/{self.current_year}/info/"
  442. @property
  443. def logs_dir(self) -> str:
  444. return f"{self.base_dir}/Logs/"
  445. @property
  446. def export_invalid_filename(self) -> str:
  447. return f"{self.base_dir}/Export/ungueltig.csv"
  448. def export_filename_for_period(self, year: str, month: str) -> str:
  449. return f"{self.base_dir}/Export/{year}/export_{year}-{month}.xml"
  450. @staticmethod
  451. def convert_to_row(node: list[ET.Element]) -> list[str]:
  452. return [child.text for child in node]
  453. @staticmethod
  454. def convert_xml_to_csv(xmlfile: str, csvfile: str) -> bool:
  455. with open(xmlfile) as frh:
  456. record_list = ET.parse(frh).getroot().find("RecordList")
  457. header = [child.tag for child in record_list.find("Record")]
  458. bookings = [GCHR.convert_to_row(node) for node in record_list.findall("Record")]
  459. with open(csvfile, "w") as fwh:
  460. cwh = csv.writer(fwh, delimiter=";")
  461. cwh.writerow(header)
  462. cwh.writerows(bookings)
  463. return True
  464. def convert_csv_to_xml(self, csvfile: str, xmlfile: str) -> None:
  465. self.makes = {"01": "1844"}
  466. self.sites = {"01-01": "1844"}
  467. with open(csvfile, "r", encoding="latin-1") as frh:
  468. csv_reader = csv.DictReader(frh, delimiter=";")
  469. GCHR.export_skr51_xml(csv_reader, self.bookkeep_filter(), 1, list(self.sites.values())[0], xmlfile)
  470. def gchr_local() -> None:
  471. base_dir = os.getcwd() + "/../GCHR2_Testdaten/Kunden"
  472. for path in Path(base_dir).glob("*"):
  473. if path.is_dir():
  474. print(path.name)
  475. gchr_export(str(path))
  476. def gchr_export(base_dir: str) -> None:
  477. gchr = GCHR(base_dir)
  478. # gchr.export_all_periods(overwrite=True, today="2022-08-01")
  479. gchr.export_all_periods()