123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- import calendar
- import csv
- import hashlib
- import os
- import re
- import shutil
- from datetime import datetime, timedelta
- from pathlib import Path
- from typing import Any, Generator, Literal
- import pyodbc
- DSN = "dsn=GC_OPTIMA_64;uid=gaps;pwd=Gcbs12ma"
- class DatevConfig:
- base_dir: str = str(Path(__file__).resolve().parent)
- data_path: str = base_dir + "/data"
- export_path: str = base_dir + "/export/temp"
- translation_file: str = data_path + "/uebersetzungstabelle.csv"
- csv_date: datetime = datetime.now() # datetime(2023, 11, 20, 19, 2, 28, 714000)
- geschaeftsjahr_monat: int = 1
- periode: str = "202301"
- berater: int = 30612
- mandant: int = 10139
- konto_laenge: int = 5
- @property
- def datum_von(self) -> datetime:
- return datetime(int(self.periode[:4]), int(self.periode[4:]), 1)
- @property
- def datum_bis(self) -> datetime:
- year = int(self.periode[:4])
- month = int(self.periode[4:])
- end_of_month = calendar.monthrange(year, month)[1]
- return datetime(year, month, end_of_month)
- @property
- def geschaeftsjahr_beginn(self) -> datetime:
- year = int(self.periode[:4])
- if self.geschaeftsjahr_monat > datetime.now().month:
- year -= 1
- return datetime(year, self.geschaeftsjahr_monat, 1)
- @property
- def header2(self) -> str:
- res = [
- "Umsatz (ohne Soll/Haben-Kz)",
- "Soll/Haben-Kennzeichen",
- "WKZ Umsatz",
- "Kurs",
- "Basis-Umsatz",
- "WKZ Basis-Umsatz",
- "Konto",
- "Gegenkonto (ohne BU-Schlüssel)",
- "BU-Schlüssel",
- "Belegdatum",
- "Belegfeld 1",
- "Belegfeld 2",
- "Skonto",
- "Buchungstext",
- "Postensperre",
- "Diverse Adressnummer",
- "Geschäftspartnerbank",
- "Sachverhalt",
- "Zinssperre",
- "Beleglink",
- "Beleginfo - Art 1",
- "Beleginfo - Inhalt 1",
- "Beleginfo - Art 2",
- "Beleginfo - Inhalt 2",
- "Beleginfo - Art 3",
- "Beleginfo - Inhalt 3",
- "Beleginfo - Art 4",
- "Beleginfo - Inhalt 4",
- "Beleginfo - Art 5",
- "Beleginfo - Inhalt 5",
- "Beleginfo - Art 6",
- "Beleginfo - Inhalt 6",
- "Beleginfo - Art 7",
- "Beleginfo - Inhalt 7",
- "Beleginfo - Art 8",
- "Beleginfo - Inhalt 8",
- "KOST1 - Kostenstelle",
- "KOST2 - Kostenstelle",
- "Kost-Menge",
- "EU-Land u. UStID",
- "EU-Steuersatz",
- "Abw. Versteuerungsart",
- "Sachverhalt L+L",
- "Funktionsergänzung L+L",
- "BU 49 Hauptfunktionstyp",
- "BU 49 Hauptfunktionsnummer",
- "BU 49 Funktionsergänzung",
- "Zusatzinformation - Art 1",
- "Zusatzinformation- Inhalt 1",
- "Zusatzinformation - Art 2",
- "Zusatzinformation- Inhalt 2",
- "Zusatzinformation - Art 3",
- "Zusatzinformation- Inhalt 3",
- "Zusatzinformation - Art 4",
- "Zusatzinformation- Inhalt 4",
- "Zusatzinformation - Art 5",
- "Zusatzinformation- Inhalt 5",
- "Zusatzinformation - Art 6",
- "Zusatzinformation- Inhalt 6",
- "Zusatzinformation - Art 7",
- "Zusatzinformation- Inhalt 7",
- "Zusatzinformation - Art 8",
- "Zusatzinformation- Inhalt 8",
- "Zusatzinformation - Art 9",
- "Zusatzinformation- Inhalt 9",
- "Zusatzinformation - Art 10",
- "Zusatzinformation- Inhalt 10",
- "Zusatzinformation - Art 11",
- "Zusatzinformation- Inhalt 11",
- "Zusatzinformation - Art 12",
- "Zusatzinformation- Inhalt 12",
- "Zusatzinformation - Art 13",
- "Zusatzinformation- Inhalt 13",
- "Zusatzinformation - Art 14",
- "Zusatzinformation- Inhalt 14",
- "Zusatzinformation - Art 15",
- "Zusatzinformation- Inhalt 15",
- "Zusatzinformation - Art 16",
- "Zusatzinformation- Inhalt 16",
- "Zusatzinformation - Art 17",
- "Zusatzinformation- Inhalt 17",
- "Zusatzinformation - Art 18",
- "Zusatzinformation- Inhalt 18",
- "Zusatzinformation - Art 19",
- "Zusatzinformation- Inhalt 19",
- "Zusatzinformation - Art 20",
- "Zusatzinformation- Inhalt 20",
- "Stück",
- "Gewicht",
- "Zahlweise",
- "Forderungsart",
- "Veranlagungsjahr",
- "Zugeordnete Fälligkeit",
- "Skontotyp",
- "Auftragsnummer",
- "Buchungstyp",
- "Ust-Schlüssel (Anzahlungen)",
- "EU-Land (Anzahlungen)",
- "Sachverhalt L+L (Anzahlungen)",
- "EU-Steuersatz (Anzahlungen)",
- "Erlöskonto (Anzahlungen)",
- "Herkunft-Kz",
- "Leerfeld",
- "KOST-Datum",
- "Mandatsreferenz",
- "Skontosperre",
- "Gesellschaftername",
- "Beteiligtennummer",
- "Identifikationsnummer",
- "Zeichnernummer",
- "Postensperre bis",
- "Bezeichnung SoBil-Sachverhalt",
- "Kennzeichen SoBil-Buchung",
- "Festschreibung",
- "Leistungsdatum",
- "Datum Zuord.Steuerperiode",
- ]
- return ";".join(res)
- row_template = (
- '{0};"{1}";"{2}";;;"";"{9}";"{4}";"";{5};"{6}";"";;"{7}";;"";;;;"";"";'
- + '"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"{10}";"";;"";;"";;;;;;"";"";"";"";"";"";"";"";"";"";"";"";"";'
- + '"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";;;;"";;;;"";"";;"";;'
- + ';;"";"";;"";;"";;"";"";;"";;0;;'
- )
- # '592.80;H;EUR;"15800";90900;0101;6288;Opel Bank VoST 12/22 Lagerwag;1'
- @property
- def export_file(self) -> str:
- timestamp = self.csv_date.strftime("%Y%m%d_%H%M%S")
- period = self.datum_von.strftime("%Y%m")
- return f"{self.export_path}/EXTF_Buchungsstapel_30612_10139_{period}_{timestamp}.csv"
- @property
- def header(self) -> str:
- datev_header = {
- "Datev-Format-KZ": "EXTF",
- "Versionsnummer": 510,
- "Datenkategorie": 21,
- "Formatname": "Buchungsstapel",
- "Formatversion": 7,
- "Erzeugt_am": self.csv_date.strftime("%Y%m%d%H%M%S%f")[:-3],
- "Importiert_am": "",
- "Herkunftskennzeichen": "SV",
- "Exportiert_von": "dracar",
- "Importiert_von": "",
- "Berater": self.berater,
- "Mandant": self.mandant,
- "WJ-Beginn": self.geschaeftsjahr_beginn.strftime("%Y%m%d"),
- "Sachkontenlänge": self.konto_laenge,
- "Datum_von": self.datum_von.strftime("%Y%m%d"),
- "Datum_bis": self.datum_bis.strftime("%Y%m%d"),
- "Bezeichnung": "",
- "Diktatkürzel": "HE",
- "Buchungstyp": 1,
- "Rechnungslegungszweck": "",
- "Festschreibeinformation": 1,
- "WKZ": "",
- "reserviert_1": "",
- "Derivatskennzeichen": "",
- "reserviert_2": "",
- "reserviert_3": "",
- "SKR": "",
- "Branchenlösung-Id": "",
- "reserviert_4": "",
- "reserviert_5": "",
- "Anwendungsinformation": "",
- }
- template = (
- '"EXTF";{Versionsnummer};{Datenkategorie};"Buchungsstapel";{Formatversion};{Erzeugt_am};'
- + ';"SV";"dracar";"";{Berater};{Mandant};{WJ-Beginn};{Sachkontenlänge};{Datum_von};{Datum_bis};"";"HE";1;;1;"";;"";;;"";;;"";""'
- )
- return template.format(**datev_header)
- def get_translation(cfg: DatevConfig) -> dict[str, str]:
- translation = {}
- with Path(cfg.translation_file).open("r", encoding="latin-1") as frh:
- for line in csv.reader(frh, delimiter=";"):
- acct_no = line[0][:4] + "0"
- acct_details = "11" + line[0][11:].replace("-", "")
- translation[line[2]] = (acct_no, acct_details)
- return translation
- def from_database(period) -> Generator[list[str], Any, None]:
- with pyodbc.connect(DSN) as conn:
- cursor = conn.cursor()
- query = (
- "SELECT * FROM [import].[DATEV_Buchungsstapel] "
- + f"WHERE [BOOKKEEP_PERIOD] = '{period}' ORDER BY [BOOKKEEP_DATE], [UNIQUE_IDENT]"
- )
- cursor.execute(query)
- for row in cursor.fetchall():
- yield list(map(str, row[:9]))
- def from_csv(import_file) -> Generator[list[str], Any, None]:
- with import_file.open("r", encoding="latin-1") as frh:
- csv_reader = csv.reader(frh, delimiter=";")
- next(csv_reader) # ignore header
- for row in csv_reader:
- yield row
- def export_extf(period: str, import_method: Literal["csv", "db"] = "csv") -> None:
- cfg = DatevConfig()
- cfg.periode = period
- translation = get_translation(cfg)
- if import_method == "csv":
- import_file = Path(f"datev/data/{period}.csv")
- cfg.csv_date = datetime.fromtimestamp(import_file.stat().st_mtime)
- get_row = from_csv(import_file)
- else:
- get_row = from_database(cfg.periode)
- missing = []
- with Path(cfg.export_file).open("w", encoding="latin-1", newline="") as fwh:
- fwh.write(cfg.header + "\r\n")
- fwh.write(cfg.header2 + "\r\n")
- for row in get_row:
- row[0] = row[0].replace(".", ",")
- row.extend(translation.get(row[3], (row[3], "11000000")))
- if row[9] == row[3]:
- missing.append(row[3])
- fwh.write(cfg.row_template.format(*row) + "\r\n")
- # print(set(missing))
- def export_all_periods() -> None:
- dt = datetime.now()
- prev = str(dt.year - 1)
- periods = [f"{prev}{x:02}" for x in range(1, 13)] + [f"{dt.year}{x:02}" for x in range(1, dt.month + 1)]
- for p in periods:
- export_extf(p, "db")
- def extf_get_hash(filename):
- with open(filename, "r", encoding="latin-1") as frh1:
- frh1.readline() # ignore header
- data = frh1.read()
- return calculate_sha256(data)
- def extf_files_equal_content(file1, file2):
- with open(file1, "r", encoding="latin-1") as frh1:
- frh1.readline() # ignore header
- data1 = frh1.read()
- with open(file2, "r", encoding="latin-1") as frh2:
- frh2.readline() # ignore header
- data2 = frh2.read()
- print(calculate_sha256(data1))
- print(calculate_sha256(data2))
- return calculate_sha256(data1) == calculate_sha256(data2)
- def calculate_sha256(data) -> str:
- return hashlib.sha256(data.encode()).hexdigest()
- def test_content():
- print(
- extf_files_equal_content(
- "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
- "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
- )
- )
- print(
- extf_files_equal_content(
- "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240222_155629.csv",
- "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
- )
- )
- print(
- extf_files_equal_content(
- "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240514_112734.csv",
- "datev/export/EXTF_Buchungsstapel_30612_10139_202312_20240515_104021.csv",
- )
- )
- def archive_files():
- last_week = (datetime.now() - timedelta(days=6)).timestamp()
- for file in Path("datev/export").glob("*.csv"):
- if file.stat().st_ctime < last_week:
- file.unlink()
- archive_path = Path("datev/export/Archiv")
- for file in Path("datev/export/temp").glob("*.csv"):
- p = re.search(r"_(\d{6})_", file.name)
- if not p:
- continue
- period = p[1]
- target = archive_path / period[:4] / period
- os.makedirs(target, exist_ok=True)
- file_hash = extf_get_hash(file)
- if has_identical_file(target, file_hash):
- file.unlink()
- continue
- shutil.copy(file, archive_path.parent / file.name)
- file.rename(target / file.name)
- def has_identical_file(target: Path, file_hash: str) -> bool:
- for archived_file in Path(target).glob("*.csv"):
- if extf_get_hash(archived_file) == file_hash:
- return True
- return False
- if __name__ == "__main__":
- export_all_periods()
- archive_files()
- # os.makedirs(Path(filename).parent.joinpath("info"), exist_ok=True)
|