mt940-py 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: mt940-py
3
+ Version: 0.1.5
4
+ Summary: Narzędzie do walidacji i konwersji wyciągów bankowych do formatu MT940 (wariant ZBP).
5
+ Author-email: TheUndefined <undefine@aramin.net>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: mt-940>=4.30.0
@@ -0,0 +1,25 @@
1
+ # mt940-py
2
+
3
+ Biblioteka i narzędzie do walidacji oraz konwersji wyciągów bankowych do formatu MT940 (wariant polski ZBP).
4
+
5
+ ## Główne funkcjonalności
6
+ - **Walidacja**: Sprawdzanie spójności logicznej plików MT940 (sumy kontrolne sald i transakcji).
7
+ - **Konwersja**: Tworzenie plików MT940 z wyciągów CSV (obecnie wspiera mBank).
8
+ - **Eksport**: Konwersja MT940 do czytelnego formatu CSV.
9
+ - **Interfejsy**: Biblioteka Python, CLI oraz nowoczesne GUI (CustomTkinter).
10
+
11
+ ## Instalacja
12
+ ```bash
13
+ pip install mt940-py
14
+ ```
15
+
16
+ ## Użycie CLI
17
+ ```bash
18
+ mt940-val gui # Uruchomienie interfejsu graficznego
19
+ mt940-val validate # Walidacja pliku
20
+ mt940-val convert # Konwersja CSV -> MT940
21
+ mt940-val export # Eksport MT940 -> CSV
22
+ ```
23
+
24
+ ## Rozwój
25
+ Projekt używa `black`, `ruff` i `pytest` do zapewnienia wysokiej jakości kodu.
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "mt940-py"
3
+ version = "0.1.5"
4
+ description = "Narzędzie do walidacji i konwersji wyciągów bankowych do formatu MT940 (wariant ZBP)."
5
+ authors = [
6
+ { name = "TheUndefined", email = "undefine@aramin.net" }
7
+ ]
8
+ dependencies = [
9
+ "mt-940>=4.30.0",
10
+ ]
11
+ requires-python = ">=3.12"
12
+
13
+ [project.scripts]
14
+ mt940-val = "mt940_py.main:main"
15
+
16
+ [build-system]
17
+ requires = ["setuptools>=61.0"]
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [tool.black]
21
+ line-length = 120
22
+ target-version = ['py312']
23
+
24
+ [tool.ruff]
25
+ line-length = 120
26
+ target-version = "py312"
27
+
28
+ [tool.mypy]
29
+ python_version = "3.12"
30
+ strict = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,162 @@
1
+ import csv
2
+ import datetime
3
+ import io
4
+ import re
5
+ from typing import List
6
+
7
+ class MT940Converter:
8
+ """Konwerter plików CSV (mBank) do formatu MT940 (ZBP) zgodnego z mBank/Insert."""
9
+
10
+ def __init__(self, encoding: str = "cp1250") -> None:
11
+ self.encoding = encoding
12
+
13
+ def _clean_amount_str(self, val: str) -> str:
14
+ if not val:
15
+ return "0.0"
16
+ cleaned = val.replace(" PLN", "").replace(" ", "").replace(",", ".").replace("\xa0", "")
17
+ cleaned = re.sub(r"[^\d.-]", "", cleaned)
18
+ return cleaned
19
+
20
+ def _format_amount(self, amount: float) -> str:
21
+ return f"{abs(amount):.2f}".replace(".", ",")
22
+
23
+ def _format_date(self, date_str: str) -> str:
24
+ try:
25
+ dt = datetime.datetime.strptime(date_str, "%Y-%m-%d")
26
+ return dt.strftime("%y%m%d")
27
+ except (ValueError, TypeError):
28
+ return ""
29
+
30
+ def _wrap_text(self, text: str, length: int) -> List[str]:
31
+ if not text:
32
+ return []
33
+ return [text[i : i + length] for i in range(0, len(text), length)]
34
+
35
+ def convert(self, csv_content: str) -> str:
36
+ lines = [line.strip() for line in csv_content.splitlines()]
37
+ header_index = -1
38
+ for i, line in enumerate(lines):
39
+ if "#Data księgowania;" in line:
40
+ header_index = i
41
+ break
42
+
43
+ if header_index == -1:
44
+ raise ValueError("Nie znaleziono nagłówka transakcji w pliku CSV.")
45
+
46
+ account_number = ""
47
+ initial_balance = 0.0
48
+ currency = "PLN"
49
+ statement_date = ""
50
+
51
+ for i, line in enumerate(lines[:header_index]):
52
+ if "#Numer rachunku" in line:
53
+ content = line
54
+ if i + 1 < header_index and (len(line.split(";")) <= 1 or not line.split(";")[1].strip()):
55
+ content = lines[i + 1]
56
+ match = re.search(r"(\d[\s\d]{20,})", content)
57
+ if match:
58
+ account_number = re.sub(r"[^\d]", "", match.group(1))
59
+
60
+ if "#Saldo początkowe" in line:
61
+ content = line
62
+ if i + 1 < header_index and (len(line.split(";")) <= 1 or not line.split(";")[1].strip()):
63
+ content = lines[i + 1]
64
+ try:
65
+ parts = content.split(";")
66
+ val_str = self._clean_amount_str(parts[1] if len(parts) > 1 else parts[0])
67
+ initial_balance = float(val_str)
68
+ except (IndexError, ValueError):
69
+ pass
70
+
71
+ f = io.StringIO("\n".join(lines[header_index:]))
72
+ reader = csv.DictReader(f, delimiter=";")
73
+
74
+ # Przygotowanie transakcji do późniejszego użycia (potrzebujemy daty pierwszej transakcji)
75
+ rows = []
76
+ for row in reader:
77
+ raw_date = row.get("#Data księgowania")
78
+ if raw_date and re.match(r"\d{4}-\d{2}-\d{2}", raw_date):
79
+ rows.append(row)
80
+
81
+ if rows and not statement_date:
82
+ statement_date = self._format_date(rows[0]["#Data księgowania"])
83
+ else:
84
+ statement_date = datetime.datetime.now().strftime("%y%m%d")
85
+
86
+ output = []
87
+ # :20:
88
+ output.append(":20:MT940")
89
+
90
+ # :25:
91
+ if account_number:
92
+ output.append(f":25:/PL{account_number}")
93
+ else:
94
+ output.append(":25:/UNKNOWN_ACCOUNT")
95
+
96
+ # :28C: Numer wyciągu (mBank używa daty YYMMDD)
97
+ output.append(f":28C:{statement_date}")
98
+
99
+ # :60F: Saldo początkowe
100
+ sign_60 = "C" if initial_balance >= 0 else "D"
101
+ output.append(f":60F:{sign_60}{statement_date}{currency}{self._format_amount(initial_balance)}")
102
+
103
+ current_balance = initial_balance
104
+
105
+ for idx, row in enumerate(rows):
106
+ date_val = self._format_date(row["#Data księgowania"])
107
+ amount = float(self._clean_amount_str(row.get("#Kwota", "0")))
108
+ current_balance += amount
109
+
110
+ # :61: Linia transakcji
111
+ # Dodajemy numer referencyjny na końcu (np. SD95 + numer kolejny)
112
+ sign_61 = "C" if amount >= 0 else "D"
113
+ ref_num = f"{idx+1:011d}"
114
+ output.append(f":61:{date_val}{date_val[2:]}{sign_61}{self._format_amount(amount)}SD95{ref_num}")
115
+
116
+ # :86: Struktura mBanku (podwójny tag :86: i brak :86: w kolejnych liniach pola)
117
+ output.append(":86:SD95")
118
+
119
+ operation_type = row.get("#Opis operacji", "")
120
+ title = row.get("#Tytuł", "")
121
+
122
+ # Budujemy linię 2 pola 86
123
+ details_line2 = f"SD95~00BD95{operation_type}"
124
+ # Kolejne linie tytułu i danych
125
+ details_remaining = []
126
+
127
+ title_parts = self._wrap_text(title, 27)
128
+ for i, part in enumerate(title_parts[:8]):
129
+ details_remaining.append(f"~{20+i}{part}")
130
+
131
+ contractor = row.get("#Nadawca/Odbiorca", "")
132
+ contractor_parts = self._wrap_text(contractor, 35)
133
+ if len(contractor_parts) > 0:
134
+ details_remaining.append(f"~32{contractor_parts[0]}")
135
+ if len(contractor_parts) > 1:
136
+ details_remaining.append(f"~33{contractor_parts[1]}")
137
+
138
+ acc = row.get("#Numer konta", "").replace("'", "").strip()
139
+ if acc:
140
+ acc_clean = re.sub(r"[^\d]", "", acc)
141
+ if acc_clean:
142
+ details_remaining.append(f"~31{acc_clean}")
143
+ details_remaining.append(f"~38PL{acc_clean}")
144
+
145
+ # Składamy wszystko w całość zgodnie z formatem mBanku
146
+ # Pierwsza linia po :86: (druga w sumie)
147
+ raw_full_86 = "".join(details_remaining)
148
+ wrapped_86 = self._wrap_text(raw_full_86, 65)
149
+
150
+ output.append(f":86:{details_line2}")
151
+ for line in wrapped_86:
152
+ output.append(line)
153
+
154
+ # :62F:
155
+ sign_62 = "C" if current_balance >= 0 else "D"
156
+ # Data salda końcowego z ostatniej transakcji lub dzisiejsza
157
+ last_date = self._format_date(rows[-1]["#Data księgowania"]) if rows else statement_date
158
+ output.append(f":62F:{sign_62}{last_date}{currency}{self._format_amount(current_balance)}")
159
+
160
+ # Dodajemy znak końca pliku (opcjonalnie, ale mBank często go nie ma, SWIFT wymaga tylko pól)
161
+ # Łączymy używając CRLF
162
+ return "\r\n".join(output) + "\r\n"
@@ -0,0 +1,77 @@
1
+ import csv
2
+ import io
3
+ import re
4
+ import mt940
5
+ from mt940_py.validator import MT940Validator
6
+
7
+
8
+ class MT940ToCSVExporter:
9
+ """Eksporter plików MT940 do formatu CSV."""
10
+
11
+ def __init__(self) -> None:
12
+ self.validator = MT940Validator()
13
+
14
+ def _clean_text(self, text: str) -> str:
15
+ """Usuwa znaki nowej linii i nadmiarowe spacje."""
16
+ if not text:
17
+ return ""
18
+ # Zamieniamy nową linię na spację
19
+ cleaned = text.replace("\n", " ").replace("\r", " ")
20
+ # Usuwamy wielokrotne spacje
21
+ cleaned = re.sub(r"\s+", " ", cleaned)
22
+ return cleaned.strip()
23
+
24
+ def export(self, mt940_path: str) -> str:
25
+ statements = mt940.parse(mt940_path)
26
+
27
+ output = io.StringIO()
28
+ fieldnames = [
29
+ "Data księgowania",
30
+ "Kwota",
31
+ "Waluta",
32
+ "Typ",
33
+ "Kontrahent",
34
+ "Rachunek kontrahenta",
35
+ "Tytuł",
36
+ "Szczegóły RAW",
37
+ ]
38
+
39
+ writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=";")
40
+ writer.writeheader()
41
+
42
+ if hasattr(statements, "data"):
43
+ statements_list = [statements]
44
+ else:
45
+ statements_list = statements
46
+
47
+ for statement in statements_list:
48
+ for transaction in statement:
49
+ data = transaction.data
50
+ raw_86 = data.get("transaction_details", "")
51
+ parsed_86 = self.validator.parse_field_86(raw_86)
52
+
53
+ title_parts = []
54
+ for i in range(20, 28):
55
+ val = parsed_86.get(str(i))
56
+ if val:
57
+ title_parts.append(val)
58
+
59
+ contractor = f"{parsed_86.get('32', '')} {parsed_86.get('33', '')}".strip()
60
+ acc = parsed_86.get("31", parsed_86.get("38", ""))
61
+
62
+ amount = self.validator.get_amount_value(data.get("amount"))
63
+
64
+ writer.writerow(
65
+ {
66
+ "Data księgowania": data.get("date"),
67
+ "Kwota": f"{amount:.2f}",
68
+ "Waluta": data.get("currency"),
69
+ "Typ": data.get("status"),
70
+ "Kontrahent": self._clean_text(contractor),
71
+ "Rachunek kontrahenta": self._clean_text(acc),
72
+ "Tytuł": self._clean_text(" ".join(title_parts)),
73
+ "Szczegóły RAW": self._clean_text(raw_86),
74
+ }
75
+ )
76
+
77
+ return output.getvalue()
@@ -0,0 +1,115 @@
1
+ import os
2
+ import tkinter as tk
3
+ from tkinter import filedialog, messagebox
4
+ import customtkinter as ctk
5
+ from mt940_py.converter import MT940Converter
6
+ from mt940_py.validator import MT940Validator
7
+
8
+ # Ustawienia wyglądu
9
+ ctk.set_appearance_mode("System")
10
+ ctk.set_default_color_theme("blue")
11
+
12
+ class MT940App(ctk.CTk):
13
+ def __init__(self):
14
+ super().__init__()
15
+
16
+ self.title("MT940-py: Konwerter i Walidator")
17
+ self.geometry("600x450")
18
+
19
+ self.converter = MT940Converter()
20
+ self.validator = MT940Validator()
21
+
22
+ # Układ siatki
23
+ self.grid_columnconfigure(0, weight=1)
24
+ self.grid_rowconfigure(3, weight=1)
25
+
26
+ # Nagłówek
27
+ self.label = ctk.CTkLabel(self, text="Konwerter mBank CSV -> MT940", font=ctk.CTkFont(size=20, weight="bold"))
28
+ self.label.grid(row=0, column=0, padx=20, pady=(20, 10))
29
+
30
+ # Przyciski akcji
31
+ self.button_frame = ctk.CTkFrame(self)
32
+ self.button_frame.grid(row=1, column=0, padx=20, pady=10, sticky="nsew")
33
+ self.button_frame.grid_columnconfigure((0, 1), weight=1)
34
+
35
+ self.select_button = ctk.CTkButton(self.button_frame, text="Wybierz plik CSV (mBank)", command=self.select_file)
36
+ self.select_button.grid(row=0, column=0, padx=10, pady=10)
37
+
38
+ self.val_button = ctk.CTkButton(self.button_frame, text="Waliduj plik MT940", command=self.validate_mt940, fg_color="gray")
39
+ self.val_button.grid(row=0, column=1, padx=10, pady=10)
40
+
41
+ # Status pliku
42
+ self.status_label = ctk.CTkLabel(self, text="Nie wybrano pliku", font=ctk.CTkFont(slant="italic"))
43
+ self.status_label.grid(row=2, column=0, padx=20, pady=5)
44
+
45
+ # Logi / Wyniki
46
+ self.textbox = ctk.CTkTextbox(self, width=560)
47
+ self.textbox.grid(row=3, column=0, padx=20, pady=10, sticky="nsew")
48
+ self.textbox.insert("0.0", "System gotowy.\n\n")
49
+
50
+ def log(self, message: str):
51
+ self.textbox.insert("end", f"{message}\n")
52
+ self.textbox.see("end")
53
+
54
+ def select_file(self):
55
+ file_path = filedialog.askopenfilename(
56
+ title="Wybierz plik mBank CSV",
57
+ filetypes=[("Pliki CSV", "*.csv"), ("Wszystkie pliki", "*.*")]
58
+ )
59
+ if file_path:
60
+ self.status_label.configure(text=f"Wybrano: {os.path.basename(file_path)}")
61
+ self.process_conversion(file_path)
62
+
63
+ def process_conversion(self, input_path: str):
64
+ output_path = filedialog.asksaveasfilename(
65
+ title="Zapisz jako MT940",
66
+ defaultextension=".txt",
67
+ initialfile=os.path.basename(input_path).replace(".csv", ".txt"),
68
+ filetypes=[("Pliki tekstowe", "*.txt"), ("Pliki MT940", "*.sta"), ("Wszystkie pliki", "*.*")]
69
+ )
70
+
71
+ if not output_path:
72
+ return
73
+
74
+ try:
75
+ self.log(f"Rozpoczynam konwersję: {os.path.basename(input_path)}...")
76
+ with open(input_path, "r", encoding="cp1250", errors="replace") as f:
77
+ content = f.read()
78
+
79
+ mt940_content = self.converter.convert(content)
80
+ # Zapisujemy jako UTF-8 z BOM (utf-8-sig)
81
+ with open(output_path, "w", encoding="utf-8-sig") as f:
82
+ f.write(mt940_content)
83
+
84
+ self.log(f"SUCCESS: Zapisano do {output_path}")
85
+ val_results = self.validator.validate_file(output_path)
86
+ if val_results["is_valid"]:
87
+ self.log(f"Walidacja OK: {val_results['statements_count']} wyciągów.")
88
+ messagebox.showinfo("Sukces", "Konwersja zakończona pomyślnie!")
89
+ else:
90
+ self.log("BŁĄD WALIDACJI wygenerowanego pliku!")
91
+ messagebox.showwarning("Uwaga", "Plik wygenerowany, ale zawiera błędy logiczne.")
92
+ except Exception as e:
93
+ self.log(f"ERROR: {str(e)}")
94
+ messagebox.showerror("Błąd", str(e))
95
+
96
+ def validate_mt940(self):
97
+ file_path = filedialog.askopenfilename(
98
+ title="Wybierz plik MT940",
99
+ filetypes=[("Pliki tekstowe", "*.txt"), ("Pliki MT940", "*.sta"), ("Wszystkie pliki", "*.*")]
100
+ )
101
+ if file_path:
102
+ val_results = self.validator.validate_file(file_path)
103
+ if val_results["is_valid"]:
104
+ self.log(f"SUCCESS: Plik {os.path.basename(file_path)} poprawny.")
105
+ messagebox.showinfo("OK", "Plik jest poprawny.")
106
+ else:
107
+ self.log(f"FAILURE: Plik {os.path.basename(file_path)} błędny.")
108
+ messagebox.showerror("Błąd", "Plik zawiera błędy.")
109
+
110
+ def main():
111
+ app = MT940App()
112
+ app.mainloop()
113
+
114
+ if __name__ == "__main__":
115
+ main()
@@ -0,0 +1,75 @@
1
+ import sys
2
+ import io
3
+
4
+ # Fix for libraries that expect sys.stdout/stderr to be present (like mt-940)
5
+ # especially when compiled with --noconsole
6
+ if sys.stdout is None:
7
+ sys.stdout = io.StringIO()
8
+ if sys.stderr is None:
9
+ sys.stderr = io.StringIO()
10
+
11
+ import argparse
12
+ import os
13
+ from mt940_py.validator import MT940Validator
14
+ from mt940_py.converter import MT940Converter
15
+ from mt940_py.exporter import MT940ToCSVExporter
16
+
17
+ def main():
18
+ parser = argparse.ArgumentParser(description="Narzędzie MT940: CLI & GUI.")
19
+ subparsers = parser.add_subparsers(dest="command", help="Komenda do wykonania")
20
+
21
+ # CLI commands
22
+ subparsers.add_parser("validate", help="Waliduje plik MT940").add_argument("file")
23
+
24
+ conv_parser = subparsers.add_parser("convert", help="Konwertuje CSV to MT940")
25
+ conv_parser.add_argument("input")
26
+ conv_parser.add_argument("output")
27
+
28
+ exp_parser = subparsers.add_parser("export", help="Eksportuje MT940 to CSV")
29
+ exp_parser.add_argument("input")
30
+ exp_parser.add_argument("output")
31
+
32
+ # GUI command
33
+ subparsers.add_parser("gui", help="Uruchamia interfejs graficzny")
34
+
35
+ args = parser.parse_args()
36
+
37
+ if args.command == "gui":
38
+ from mt940_py.gui import main as run_gui
39
+ run_gui()
40
+ elif args.command == "validate":
41
+ validator = MT940Validator()
42
+ results = validator.validate_file(args.file)
43
+ if results["is_valid"]:
44
+ print(f"SUCCESS: {results['statements_count']} wyciągów, {results['transactions_count']} transakcji.")
45
+ else:
46
+ print("FAILURE: Błędy:")
47
+ for err in results["errors"]: print(f" - {err}")
48
+ sys.exit(1)
49
+ elif args.command == "convert":
50
+ try:
51
+ with open(args.input, 'r', encoding='cp1250', errors='replace') as f:
52
+ mt940_res = MT940Converter().convert(f.read())
53
+ # Zapisujemy jako UTF-8 z BOM (utf-8-sig)
54
+ with open(args.output, 'w', encoding='utf-8-sig') as f:
55
+ f.write(mt940_res)
56
+ print(f"SUCCESS: {args.output}")
57
+ except Exception as e:
58
+ print(f"ERROR: {e}")
59
+ sys.exit(1)
60
+ elif args.command == "export":
61
+ try:
62
+ csv_res = MT940ToCSVExporter().export(args.input)
63
+ with open(args.output, 'w', encoding='utf-8') as f:
64
+ f.write(csv_res)
65
+ print(f"SUCCESS: {args.output}")
66
+ except Exception as e:
67
+ print(f"ERROR: {e}")
68
+ sys.exit(1)
69
+ else:
70
+ # Default to GUI if no command
71
+ from mt940_py.gui import main as run_gui
72
+ run_gui()
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,84 @@
1
+ import re
2
+ from typing import Dict, Any, List
3
+ import mt940 # type: ignore
4
+
5
+ class MT940Validator:
6
+ """Walidator plików MT940 z uwzględnieniem polskich rozszerzeń ZBP."""
7
+
8
+ def __init__(self) -> None:
9
+ pass
10
+
11
+ def parse_field_86(self, raw_details: str) -> Dict[str, str]:
12
+ if not raw_details:
13
+ return {}
14
+ parts = re.split(r"~(\d{2})", raw_details)
15
+ parsed: Dict[str, str] = {}
16
+ if len(parts) > 1:
17
+ for i in range(1, len(parts), 2):
18
+ tag = parts[i]
19
+ value = parts[i + 1].strip() if i + 1 < len(parts) else ""
20
+ if tag in parsed:
21
+ parsed[tag] += " " + value
22
+ else:
23
+ parsed[tag] = value
24
+ return parsed
25
+
26
+ def get_amount_value(self, obj: Any) -> float:
27
+ if obj is None:
28
+ return 0.0
29
+ if hasattr(obj, "amount") and hasattr(obj.amount, "amount"):
30
+ return float(obj.amount.amount)
31
+ if hasattr(obj, "amount"):
32
+ return float(obj.amount)
33
+ try:
34
+ return float(obj)
35
+ except Exception:
36
+ return 0.0
37
+
38
+ def validate_file(self, file_path: str) -> Dict[str, Any]:
39
+ results: Dict[str, Any] = {
40
+ "is_valid": True,
41
+ "errors": [],
42
+ "statements_count": 0,
43
+ "transactions_count": 0,
44
+ }
45
+
46
+ try:
47
+ data = mt940.parse(file_path)
48
+
49
+ if hasattr(data, "data"):
50
+ statements = [data]
51
+ else:
52
+ statements = data
53
+
54
+ results["statements_count"] = len(statements)
55
+
56
+ for statement in statements:
57
+ opening = statement.data.get("final_opening_balance")
58
+ closing = statement.data.get("final_closing_balance")
59
+
60
+ initial_val = self.get_amount_value(opening)
61
+ expected_final = self.get_amount_value(closing)
62
+
63
+ actual_sum = initial_val
64
+
65
+ for transaction in statement:
66
+ results["transactions_count"] += 1
67
+ amount = transaction.data.get("amount")
68
+ actual_sum += self.get_amount_value(amount)
69
+
70
+ raw_86 = transaction.data.get("transaction_details", "")
71
+ self.parse_field_86(raw_86)
72
+
73
+ if abs(actual_sum - expected_final) > 0.01:
74
+ results["is_valid"] = False
75
+ results["errors"].append(
76
+ f"Błąd sumy kontrolnej: Saldo pocz. ({initial_val:.2f}) + transakcje = {actual_sum:.2f}, "
77
+ f"a saldo końcowe w pliku to {expected_final:.2f}"
78
+ )
79
+
80
+ except Exception as e:
81
+ results["is_valid"] = False
82
+ results["errors"].append(f"Błąd krytyczny parsowania: {str(e)}")
83
+
84
+ return results
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: mt940-py
3
+ Version: 0.1.5
4
+ Summary: Narzędzie do walidacji i konwersji wyciągów bankowych do formatu MT940 (wariant ZBP).
5
+ Author-email: TheUndefined <undefine@aramin.net>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: mt-940>=4.30.0
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/mt940_py/converter.py
4
+ src/mt940_py/exporter.py
5
+ src/mt940_py/gui.py
6
+ src/mt940_py/main.py
7
+ src/mt940_py/validator.py
8
+ src/mt940_py.egg-info/PKG-INFO
9
+ src/mt940_py.egg-info/SOURCES.txt
10
+ src/mt940_py.egg-info/dependency_links.txt
11
+ src/mt940_py.egg-info/entry_points.txt
12
+ src/mt940_py.egg-info/requires.txt
13
+ src/mt940_py.egg-info/top_level.txt
14
+ tests/test_converter.py
15
+ tests/test_validator.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mt940-val = mt940_py.main:main
@@ -0,0 +1 @@
1
+ mt-940>=4.30.0
@@ -0,0 +1 @@
1
+ mt940_py
@@ -0,0 +1,35 @@
1
+ import pytest
2
+ from mt940_py.converter import MT940Converter
3
+
4
+ def test_clean_amount_str():
5
+ conv = MT940Converter()
6
+ assert conv._clean_amount_str("1 234,56 PLN") == "1234.56"
7
+ assert conv._clean_amount_str("-75,82") == "-75.82"
8
+
9
+ def test_format_amount():
10
+ conv = MT940Converter()
11
+ assert conv._format_amount(1234.56) == "1234,56"
12
+
13
+ def test_format_date():
14
+ conv = MT940Converter()
15
+ assert conv._format_date("2026-01-10") == "260110"
16
+
17
+ def test_basic_conversion_structure():
18
+ # Bardziej realistyczny format mBanku
19
+ csv_content = (
20
+ "#Numer rachunku;PL12345678901234567890123456;\n"
21
+ "#Saldo początkowe;100,00 PLN;\n"
22
+ "#Data księgowania;#Data operacji;#Opis operacji;#Tytuł;#Nadawca/Odbiorca;#Numer konta;#Kwota;#Saldo po operacji;\n"
23
+ "2026-01-10;2026-01-10;OPIS;TYTUL;NADAWCA;112233;-10,00;90,00;\n"
24
+ )
25
+ conv = MT940Converter()
26
+ result = conv.convert(csv_content)
27
+
28
+ assert ":20:MT940" in result
29
+ assert "/PL12345678901234567890123456" in result
30
+ assert ":60F:C260110PLN100,00" in result
31
+ assert "D10,00" in result
32
+ assert ":62F:C260110PLN90,00" in result
33
+ assert "\r\n" in result
34
+ # Sprawdzenie podwójnego 86
35
+ assert ":86:SD95\r\n:86:SD95~00BD95OPIS" in result
@@ -0,0 +1,47 @@
1
+ import pytest
2
+ import os
3
+ from mt940_py.validator import MT940Validator
4
+ from mt940_py.converter import MT940Converter
5
+
6
+ def test_validator_success():
7
+ csv_content = (
8
+ "#Numer rachunku;1234567890\n"
9
+ "#Saldo początkowe;100,00\n"
10
+ "#Data księgowania;Tytuł;Kwota\n"
11
+ "2026-01-10;TEST;50,00\n"
12
+ )
13
+ conv = MT940Converter()
14
+ mt940_data = conv.convert(csv_content)
15
+
16
+ temp_path = "tests/temp_test.txt"
17
+ with open(temp_path, "w", encoding="utf-8") as f:
18
+ f.write(mt940_data)
19
+
20
+ validator = MT940Validator()
21
+ results = validator.validate_file(temp_path)
22
+
23
+ if os.path.exists(temp_path):
24
+ os.remove(temp_path)
25
+
26
+ assert results["is_valid"] is True
27
+
28
+ def test_validator_error():
29
+ bad_mt940 = (
30
+ ":20:MT940\r\n"
31
+ ":25:/PL123\r\n"
32
+ ":60F:C260101PLN100,00\r\n"
33
+ ":61:2601010101C50,00SD95\r\n"
34
+ ":86:TEST\r\n"
35
+ ":62F:C260101PLN200,00\r\n"
36
+ )
37
+ temp_path = "tests/temp_bad.txt"
38
+ with open(temp_path, "w", encoding="utf-8") as f:
39
+ f.write(bad_mt940)
40
+
41
+ validator = MT940Validator()
42
+ results = validator.validate_file(temp_path)
43
+
44
+ if os.path.exists(temp_path):
45
+ os.remove(temp_path)
46
+
47
+ assert results["is_valid"] is False