export_extf.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. from datetime import datetime
  2. import calendar
  3. import csv
  4. import hashlib
  5. from typing import Any, Generator, Literal
  6. import pyodbc
  7. from pathlib import Path
  8. DSN = "dsn=GC_OPTIMA_64;uid=gaps;pwd=Gcbs12ma"
  9. class DatevConfig:
  10. base_dir: str = str(Path(__file__).resolve().parent)
  11. data_path: str = base_dir + "/data"
  12. export_path: str = base_dir + "/export"
  13. translation_file: str = data_path + "/uebersetzungstabelle.csv"
  14. csv_date: datetime = datetime.now() # datetime(2023, 11, 20, 19, 2, 28, 714000)
  15. geschaeftsjahr_monat: int = 1
  16. periode: str = "202301"
  17. berater: int = 30612
  18. mandant: int = 10139
  19. konto_laenge: int = 5
  20. @property
  21. def datum_von(self) -> datetime:
  22. return datetime(int(self.periode[:4]), int(self.periode[4:]), 1)
  23. @property
  24. def datum_bis(self) -> datetime:
  25. year = int(self.periode[:4])
  26. month = int(self.periode[4:])
  27. end_of_month = calendar.monthrange(year, month)[1]
  28. return datetime(year, month, end_of_month)
  29. @property
  30. def geschaeftsjahr_beginn(self) -> datetime:
  31. year = int(self.periode[:4])
  32. if self.geschaeftsjahr_monat > datetime.now().month:
  33. year -= 1
  34. return datetime(year, self.geschaeftsjahr_monat, 1)
  35. @property
  36. def header2(self) -> str:
  37. res = [
  38. "Umsatz (ohne Soll/Haben-Kz)",
  39. "Soll/Haben-Kennzeichen",
  40. "WKZ Umsatz",
  41. "Kurs",
  42. "Basis-Umsatz",
  43. "WKZ Basis-Umsatz",
  44. "Konto",
  45. "Gegenkonto (ohne BU-Schlüssel)",
  46. "BU-Schlüssel",
  47. "Belegdatum",
  48. "Belegfeld 1",
  49. "Belegfeld 2",
  50. "Skonto",
  51. "Buchungstext",
  52. "Postensperre",
  53. "Diverse Adressnummer",
  54. "Geschäftspartnerbank",
  55. "Sachverhalt",
  56. "Zinssperre",
  57. "Beleglink",
  58. "Beleginfo - Art 1",
  59. "Beleginfo - Inhalt 1",
  60. "Beleginfo - Art 2",
  61. "Beleginfo - Inhalt 2",
  62. "Beleginfo - Art 3",
  63. "Beleginfo - Inhalt 3",
  64. "Beleginfo - Art 4",
  65. "Beleginfo - Inhalt 4",
  66. "Beleginfo - Art 5",
  67. "Beleginfo - Inhalt 5",
  68. "Beleginfo - Art 6",
  69. "Beleginfo - Inhalt 6",
  70. "Beleginfo - Art 7",
  71. "Beleginfo - Inhalt 7",
  72. "Beleginfo - Art 8",
  73. "Beleginfo - Inhalt 8",
  74. "KOST1 - Kostenstelle",
  75. "KOST2 - Kostenstelle",
  76. "Kost-Menge",
  77. "EU-Land u. UStID",
  78. "EU-Steuersatz",
  79. "Abw. Versteuerungsart",
  80. "Sachverhalt L+L",
  81. "Funktionsergänzung L+L",
  82. "BU 49 Hauptfunktionstyp",
  83. "BU 49 Hauptfunktionsnummer",
  84. "BU 49 Funktionsergänzung",
  85. "Zusatzinformation - Art 1",
  86. "Zusatzinformation- Inhalt 1",
  87. "Zusatzinformation - Art 2",
  88. "Zusatzinformation- Inhalt 2",
  89. "Zusatzinformation - Art 3",
  90. "Zusatzinformation- Inhalt 3",
  91. "Zusatzinformation - Art 4",
  92. "Zusatzinformation- Inhalt 4",
  93. "Zusatzinformation - Art 5",
  94. "Zusatzinformation- Inhalt 5",
  95. "Zusatzinformation - Art 6",
  96. "Zusatzinformation- Inhalt 6",
  97. "Zusatzinformation - Art 7",
  98. "Zusatzinformation- Inhalt 7",
  99. "Zusatzinformation - Art 8",
  100. "Zusatzinformation- Inhalt 8",
  101. "Zusatzinformation - Art 9",
  102. "Zusatzinformation- Inhalt 9",
  103. "Zusatzinformation - Art 10",
  104. "Zusatzinformation- Inhalt 10",
  105. "Zusatzinformation - Art 11",
  106. "Zusatzinformation- Inhalt 11",
  107. "Zusatzinformation - Art 12",
  108. "Zusatzinformation- Inhalt 12",
  109. "Zusatzinformation - Art 13",
  110. "Zusatzinformation- Inhalt 13",
  111. "Zusatzinformation - Art 14",
  112. "Zusatzinformation- Inhalt 14",
  113. "Zusatzinformation - Art 15",
  114. "Zusatzinformation- Inhalt 15",
  115. "Zusatzinformation - Art 16",
  116. "Zusatzinformation- Inhalt 16",
  117. "Zusatzinformation - Art 17",
  118. "Zusatzinformation- Inhalt 17",
  119. "Zusatzinformation - Art 18",
  120. "Zusatzinformation- Inhalt 18",
  121. "Zusatzinformation - Art 19",
  122. "Zusatzinformation- Inhalt 19",
  123. "Zusatzinformation - Art 20",
  124. "Zusatzinformation- Inhalt 20",
  125. "Stück",
  126. "Gewicht",
  127. "Zahlweise",
  128. "Forderungsart",
  129. "Veranlagungsjahr",
  130. "Zugeordnete Fälligkeit",
  131. "Skontotyp",
  132. "Auftragsnummer",
  133. "Buchungstyp",
  134. "Ust-Schlüssel (Anzahlungen)",
  135. "EU-Land (Anzahlungen)",
  136. "Sachverhalt L+L (Anzahlungen)",
  137. "EU-Steuersatz (Anzahlungen)",
  138. "Erlöskonto (Anzahlungen)",
  139. "Herkunft-Kz",
  140. "Leerfeld",
  141. "KOST-Datum",
  142. "Mandatsreferenz",
  143. "Skontosperre",
  144. "Gesellschaftername",
  145. "Beteiligtennummer",
  146. "Identifikationsnummer",
  147. "Zeichnernummer",
  148. "Postensperre bis",
  149. "Bezeichnung SoBil-Sachverhalt",
  150. "Kennzeichen SoBil-Buchung",
  151. "Festschreibung",
  152. "Leistungsdatum",
  153. "Datum Zuord.Steuerperiode",
  154. ]
  155. return ";".join(res)
  156. row_template = (
  157. '{0};"{1}";"{2}";;;"";"{9}";"{4}";"";{5};"{6}";"";;"{7}";;"";;;;"";"";'
  158. + '"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"{10}";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";'
  159. + '"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;'
  160. + ';;"";"";;"";;"";;"";"";;"";;0;;'
  161. )
  162. # '592.80;H;EUR;"15800";90900;0101;6288;Opel Bank VoST 12/22 Lagerwag;1'
  163. @property
  164. def export_file(self) -> str:
  165. timestamp = self.csv_date.strftime("%Y%m%d_%H%M%S")
  166. period = self.datum_von.strftime("%Y%m")
  167. return f"{self.export_path}/EXTF_Buchungsstapel_30612_10139_{period}_{timestamp}.csv"
  168. @property
  169. def header(self) -> str:
  170. datev_header = {
  171. "Datev-Format-KZ": "EXTF",
  172. "Versionsnummer": 510,
  173. "Datenkategorie": 21,
  174. "Formatname": "Buchungsstapel",
  175. "Formatversion": 7,
  176. "Erzeugt_am": self.csv_date.strftime("%Y%m%d%H%M%S%f")[:-3],
  177. "Importiert_am": "",
  178. "Herkunftskennzeichen": "SV",
  179. "Exportiert_von": "dracar",
  180. "Importiert_von": "",
  181. "Berater": self.berater,
  182. "Mandant": self.mandant,
  183. "WJ-Beginn": self.geschaeftsjahr_beginn.strftime("%Y%m%d"),
  184. "Sachkontenlänge": self.konto_laenge,
  185. "Datum_von": self.datum_von.strftime("%Y%m%d"),
  186. "Datum_bis": self.datum_bis.strftime("%Y%m%d"),
  187. "Bezeichnung": "",
  188. "Diktatkürzel": "HE",
  189. "Buchungstyp": 1,
  190. "Rechnungslegungszweck": "",
  191. "Festschreibeinformation": 1,
  192. "WKZ": "",
  193. "reserviert_1": "",
  194. "Derivatskennzeichen": "",
  195. "reserviert_2": "",
  196. "reserviert_3": "",
  197. "SKR": "",
  198. "Branchenlösung-Id": "",
  199. "reserviert_4": "",
  200. "reserviert_5": "",
  201. "Anwendungsinformation": "",
  202. }
  203. template = (
  204. '"EXTF";{Versionsnummer};{Datenkategorie};"Buchungsstapel";{Formatversion};{Erzeugt_am};'
  205. + ';"SV";"dracar";"";{Berater};{Mandant};{WJ-Beginn};{Sachkontenlänge};{Datum_von};{Datum_bis};"";"HE";1;;1;"";;"";;;"";;;"";""'
  206. )
  207. return template.format(**datev_header)
  208. def get_translation(cfg: DatevConfig) -> dict[str, str]:
  209. translation = {}
  210. with Path(cfg.translation_file).open("r", encoding="latin-1") as frh:
  211. for line in csv.reader(frh, delimiter=";"):
  212. acct_no = line[0][:4] + "0"
  213. acct_details = "11" + line[0][11:].replace("-", "")
  214. translation[line[2]] = (acct_no, acct_details)
  215. return translation
  216. def from_database(period) -> Generator[list[str], Any, None]:
  217. with pyodbc.connect(DSN) as conn:
  218. cursor = conn.cursor()
  219. query = (
  220. "SELECT * FROM [import].[DATEV_Buchungsstapel] "
  221. + f"WHERE [BOOKKEEP_PERIOD] = '{period}' ORDER BY [BOOKKEEP_DATE], [UNIQUE_IDENT]"
  222. )
  223. cursor.execute(query)
  224. for row in cursor.fetchall():
  225. yield list(map(str, row[:9]))
  226. def from_csv(import_file) -> Generator[list[str], Any, None]:
  227. with import_file.open("r", encoding="latin-1") as frh:
  228. csv_reader = csv.reader(frh, delimiter=";")
  229. next(csv_reader) # ignore header
  230. for row in csv_reader:
  231. yield row
  232. def export_extf(period: str, import_method: Literal["csv", "db"] = "csv") -> None:
  233. cfg = DatevConfig()
  234. cfg.periode = period
  235. translation = get_translation(cfg)
  236. if import_method == "csv":
  237. import_file = Path(f"datev/data/{period}.csv")
  238. cfg.csv_date = datetime.fromtimestamp(import_file.stat().st_mtime)
  239. get_row = from_csv(import_file)
  240. else:
  241. get_row = from_database(cfg.periode)
  242. missing = []
  243. with Path(cfg.export_file).open("w", encoding="latin-1", newline="") as fwh:
  244. fwh.write(cfg.header + "\r\n")
  245. fwh.write(cfg.header2 + "\r\n")
  246. for row in get_row:
  247. row[0] = row[0].replace(".", ",")
  248. row.extend(translation.get(row[3], (row[3], "11000000")))
  249. if row[9] == row[3]:
  250. missing.append(row[3])
  251. fwh.write(cfg.row_template.format(*row) + "\r\n")
  252. # print(set(missing))
  253. def export_all_periods() -> None:
  254. dt = datetime.now()
  255. prev = str(dt.year - 1)
  256. periods = [f"{prev}{x:02}" for x in range(1, 13)] + [f"{dt.year}{x:02}" for x in range(1, dt.month + 1)]
  257. for year, month in periods:
  258. period = f"{year}{month}"
  259. export_extf(period, "db")
  260. def extf_files_equal_content(file1, file2):
  261. with open(file1, "r", encoding="latin-1") as frh1:
  262. frh1.readline() # ignore header
  263. data1 = frh1.read()
  264. with open(file2, "r", encoding="latin-1") as frh2:
  265. frh2.readline() # ignore header
  266. data2 = frh2.read()
  267. print(calculate_sha256(data1))
  268. print(calculate_sha256(data2))
  269. return calculate_sha256(data1) == calculate_sha256(data2)
  270. def calculate_sha256(data) -> str:
  271. return hashlib.sha256(data.encode()).hexdigest()
  272. if __name__ == "__main__":
  273. # export_all_periods()
  274. print(
  275. extf_files_equal_content(
  276. "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
  277. "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
  278. )
  279. )
  280. print(
  281. extf_files_equal_content(
  282. "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240222_155629.csv",
  283. "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
  284. )
  285. )
  286. print(
  287. extf_files_equal_content(
  288. "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
  289. "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240515_104021.csv",
  290. )
  291. )
  292. # os.makedirs(Path(filename).parent.joinpath("info"), exist_ok=True)