beangulp-yapeal 0.1.0__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.
- beangulp_yapeal-0.1.0/.gitignore +12 -0
- beangulp_yapeal-0.1.0/PKG-INFO +13 -0
- beangulp_yapeal-0.1.0/README.md +3 -0
- beangulp_yapeal-0.1.0/pyproject.toml +21 -0
- beangulp_yapeal-0.1.0/src/beangulp_yapeal/__init__.py +172 -0
- beangulp_yapeal-0.1.0/tests/fixture.csv +4 -0
- beangulp_yapeal-0.1.0/tests/test_yapeal_importer.py +180 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beangulp-yapeal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: beangulp importer for Yapeal CSV bank statements: drafts transactions with stable bank references for idempotent re-imports
|
|
5
|
+
Keywords: bank-statement,beancount,beangulp,csv,importer,yapeal
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: beancount>=3.0
|
|
8
|
+
Requires-Dist: beangulp>=0.2
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# beangulp-yapeal
|
|
12
|
+
|
|
13
|
+
beangulp importer for Yapeal CSV bank statements.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "beangulp-yapeal"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "beangulp importer for Yapeal CSV bank statements: drafts transactions with stable bank references for idempotent re-imports"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
keywords = ["beancount", "beangulp", "importer", "csv", "bank-statement", "yapeal"]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"beancount>=3.0",
|
|
10
|
+
"beangulp>=0.2",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = ["pytest>=8"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/beangulp_yapeal"]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""beangulp importer for Yapeal CSV bank statements.
|
|
2
|
+
|
|
3
|
+
Parses Yapeal CSV exports into beancount transaction drafts following the
|
|
4
|
+
``csvbase`` pattern (see :mod:`beangulp.importers.csvbase` and
|
|
5
|
+
``beangulp/examples/importers/csvbank.py``):
|
|
6
|
+
|
|
7
|
+
- Every draft carries the bank's transaction ID as metadata (``bank_ref:`` by
|
|
8
|
+
default). ``extract()`` skips references already present anywhere in the
|
|
9
|
+
existing ledger, so re-running an import is always safe.
|
|
10
|
+
- ``payee_rules`` — an iterable of ``(regex, account, flag)`` — draft the
|
|
11
|
+
counter leg when the payee/narration matches; anything unmatched is emitted
|
|
12
|
+
with the review flag and the cash leg only, for a human (or agent) to
|
|
13
|
+
complete.
|
|
14
|
+
|
|
15
|
+
The importer is entity-agnostic: account names and rules are constructor
|
|
16
|
+
parameters.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from decimal import Decimal, InvalidOperation
|
|
24
|
+
|
|
25
|
+
from beancount.core import data, flags
|
|
26
|
+
from beancount.core.amount import Amount
|
|
27
|
+
from beangulp import mimetypes
|
|
28
|
+
from beangulp.importers import csvbase
|
|
29
|
+
|
|
30
|
+
__all__ = ["Importer"]
|
|
31
|
+
|
|
32
|
+
_IDENTIFY_HEADER = "Transaktions-ID,Belastung oder Gutschrift,Buchungs-Datum"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _YapealAmount(csvbase.Column):
|
|
36
|
+
"""Amount from either Gutschrift (credit), Belastung (debit), or Betrag+indicator."""
|
|
37
|
+
|
|
38
|
+
def parse(self, gutschrift: str, belastung: str, betrag: str, indicator: str) -> Decimal: # type: ignore[override]
|
|
39
|
+
if gutschrift:
|
|
40
|
+
return Decimal(gutschrift)
|
|
41
|
+
if belastung:
|
|
42
|
+
return -Decimal(belastung)
|
|
43
|
+
if betrag:
|
|
44
|
+
amount = Decimal(betrag)
|
|
45
|
+
if indicator == "DEBIT":
|
|
46
|
+
amount = -amount
|
|
47
|
+
return amount
|
|
48
|
+
raise ValueError("no amount in row")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Importer(csvbase.Importer):
|
|
52
|
+
"""Yapeal CSV statement importer for one bank account."""
|
|
53
|
+
|
|
54
|
+
ref = csvbase.Column("Transaktions-ID")
|
|
55
|
+
date = csvbase.Date("Transaktions-Datum", "%d.%m.%Y") # type: ignore[assignment]
|
|
56
|
+
payee = csvbase.Column("Gegenpartei")
|
|
57
|
+
narration = csvbase.Column("Transaktions-Info")
|
|
58
|
+
amount = _YapealAmount(
|
|
59
|
+
"Gutschrift (CHF)", "Belastung (CHF)", "Betrag", "Belastung oder Gutschrift"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
account: str,
|
|
65
|
+
*,
|
|
66
|
+
iban: str | None = None,
|
|
67
|
+
currency: str = "CHF",
|
|
68
|
+
meta_key: str = "bank_ref",
|
|
69
|
+
payee_rules: Sequence[tuple[str, str, str]] = (),
|
|
70
|
+
review_flag: str = flags.FLAG_WARNING,
|
|
71
|
+
):
|
|
72
|
+
super().__init__(account, currency, flag=review_flag)
|
|
73
|
+
self._iban = re.sub(r"\s", "", iban).upper() if iban else None
|
|
74
|
+
self._meta_key = meta_key
|
|
75
|
+
self._rules = [
|
|
76
|
+
(re.compile(pattern, re.IGNORECASE), acct, flag) for pattern, acct, flag in payee_rules
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def identify(self, filepath: str) -> bool:
|
|
80
|
+
try:
|
|
81
|
+
mimetype, _ = mimetypes.guess_type(filepath)
|
|
82
|
+
if mimetype != "text/csv":
|
|
83
|
+
return False
|
|
84
|
+
with open(filepath, encoding="utf-8") as f:
|
|
85
|
+
head = f.read(65536)
|
|
86
|
+
if _IDENTIFY_HEADER not in head:
|
|
87
|
+
return False
|
|
88
|
+
if self._iban:
|
|
89
|
+
return self._iban in re.sub(r"\s", "", head).upper()
|
|
90
|
+
return True
|
|
91
|
+
except (OSError, UnicodeError):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def account(self, filepath: str) -> str:
|
|
95
|
+
return self.importer_account
|
|
96
|
+
|
|
97
|
+
def filename(self, filepath: str) -> str:
|
|
98
|
+
return "statement.csv"
|
|
99
|
+
|
|
100
|
+
def metadata(self, filepath: str, lineno: int, row: object) -> dict[str, object]:
|
|
101
|
+
meta = data.new_metadata(filepath, lineno)
|
|
102
|
+
ref = getattr(row, "ref", None)
|
|
103
|
+
if ref:
|
|
104
|
+
meta[self._meta_key] = ref
|
|
105
|
+
return meta
|
|
106
|
+
|
|
107
|
+
def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
|
|
108
|
+
seen = _existing_references(existing, self._meta_key)
|
|
109
|
+
offset = int(self.skiplines) + int(self.names) + 1
|
|
110
|
+
|
|
111
|
+
entries: data.Entries = []
|
|
112
|
+
for lineno, row in enumerate(self.read(filepath), offset):
|
|
113
|
+
if not row:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
ref = getattr(row, "ref", None)
|
|
117
|
+
if not ref or ref in seen:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
amount_val = row.amount # type: ignore[attr-defined]
|
|
122
|
+
except (ValueError, InvalidOperation):
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
meta = self.metadata(filepath, lineno, row)
|
|
126
|
+
payee = row.payee # type: ignore[attr-defined]
|
|
127
|
+
narration = row.narration # type: ignore[attr-defined]
|
|
128
|
+
date_val = row.date # type: ignore[attr-defined]
|
|
129
|
+
units = Amount(amount_val, self.currency)
|
|
130
|
+
postings = [data.Posting(self.importer_account, units, None, None, None, None)]
|
|
131
|
+
|
|
132
|
+
flag = self.flag
|
|
133
|
+
haystack = f"{payee or ''}\n{narration or ''}"
|
|
134
|
+
for pattern, counter, rule_flag in self._rules:
|
|
135
|
+
if pattern.search(haystack):
|
|
136
|
+
postings.append(
|
|
137
|
+
data.Posting(
|
|
138
|
+
counter, Amount(-amount_val, self.currency), None, None, None, None
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
flag = rule_flag
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
entries.append(
|
|
145
|
+
data.Transaction(
|
|
146
|
+
meta,
|
|
147
|
+
date_val,
|
|
148
|
+
flag,
|
|
149
|
+
payee,
|
|
150
|
+
narration,
|
|
151
|
+
data.EMPTY_SET,
|
|
152
|
+
data.EMPTY_SET,
|
|
153
|
+
postings,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
entries.sort(key=lambda e: e.date)
|
|
158
|
+
return entries
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _existing_references(existing: data.Entries, meta_key: str) -> set[str]:
|
|
162
|
+
refs: set[str] = set()
|
|
163
|
+
for entry in existing or []:
|
|
164
|
+
meta = getattr(entry, "meta", None) or {}
|
|
165
|
+
value = meta.get(meta_key)
|
|
166
|
+
if isinstance(value, str):
|
|
167
|
+
refs.add(value)
|
|
168
|
+
for posting in getattr(entry, "postings", ()) or ():
|
|
169
|
+
value = (posting.meta or {}).get(meta_key)
|
|
170
|
+
if isinstance(value, str):
|
|
171
|
+
refs.add(value)
|
|
172
|
+
return refs
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
Transaktions-ID,Belastung oder Gutschrift,Buchungs-Datum,Transaktions-Datum,Transaktions-Info,Betrag,Belastung (CHF),Gutschrift (CHF),Kontowährung,Saldo (CHF),Gegenpartei,IBAN Gegenpartei,Original Transaktionsbetrag,Transaktionswährung,Devisenkurs,Gebühr,Zahlungstyp,Letzte 4 der Kartennummer,Karteninhaber,Status der Spesen,Zahlungsreferenz,Eigene Notiz,Zahlungs-Notiz,Zahlung erfasst am,Zahlung genehmigt von,Zahlung genehmigt am,Kategorie,Karten Programm,usedTags,mentions,ultimateCreditorName,ultimateDebitorName
|
|
2
|
+
e9839ff4-e419-432a-b553-801e13c7e8f1,DEBIT,05.01.2026,05.01.2026,Debit card payment,25.50,25.50,,CHF,-25.50,ACME Hosting Inc,CH9300762011623852957,25.50,CHF,,,,,,,,Subject Test,,,,general,,,,,,,
|
|
3
|
+
f68eb96a-1776-4c4d-b099-12ef65be3b63,CREDIT,10.01.2026,10.01.2026,Invoice payment,1000.00,,1000.00,CHF,1000.00,Client AG,CH9300762011623852957,1000.00,CHF,,,,,,,,Subject Test,,,,general,,,,,,,
|
|
4
|
+
a1b2c3d4-5678-90ab-cdef-1234567890ab,DEBIT,15.01.2026,15.01.2026,Monthly subscription,45.00,45.00,,CHF,-45.00,Swisscom,CH9300762011623852957,45.00,CHF,,,,,,,,Subject Test,,,,general,,,,,,,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tests for the Yapeal CSV beangulp importer."""
|
|
2
|
+
|
|
3
|
+
from datetime import date as Date
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from beancount.core import data
|
|
8
|
+
|
|
9
|
+
from beangulp_yapeal import Importer
|
|
10
|
+
|
|
11
|
+
FIXTURE = str(Path(__file__).parent / "fixture.csv")
|
|
12
|
+
|
|
13
|
+
CASH = "Assets:Bank:Checking"
|
|
14
|
+
RULES = ((r"client ag", "Assets:Receivable", "*"),)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _importer() -> Importer:
|
|
18
|
+
return Importer(CASH, payee_rules=RULES)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_identify_accepts_yapeal_csv(tmp_path: Path) -> None:
|
|
22
|
+
assert _importer().identify(FIXTURE)
|
|
23
|
+
other = tmp_path / "not-a-statement.csv"
|
|
24
|
+
other.write_text("date,amount\n2026-01-01,1.00\n")
|
|
25
|
+
assert not _importer().identify(str(other))
|
|
26
|
+
other.write_text("Transaktions-ID,Betrag\n")
|
|
27
|
+
assert not _importer().identify(str(other))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_identify_bails_on_binary(tmp_path: Path) -> None:
|
|
31
|
+
binary = tmp_path / "binary.dat"
|
|
32
|
+
binary.write_bytes(b"\x80\x81\x82")
|
|
33
|
+
assert not _importer().identify(str(binary))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_identify_with_iban(tmp_path: Path) -> None:
|
|
37
|
+
importer = Importer(CASH, iban="CH9300762011623852957")
|
|
38
|
+
iban_csv = tmp_path / "with-iban.csv"
|
|
39
|
+
iban_csv.write_text(
|
|
40
|
+
"Transaktions-ID,Belastung oder Gutschrift,Buchungs-Datum,"
|
|
41
|
+
"Transaktions-Datum,Transaktions-Info,Betrag,Belastung (CHF),"
|
|
42
|
+
"Gutschrift (CHF),Kontowährung,Saldo (CHF),Gegenpartei,IBAN Gegenpartei,"
|
|
43
|
+
"Original Transaktionsbetrag,Transaktionswährung,Devisenkurs,Gebühr,"
|
|
44
|
+
"Zahlungstyp,Letzte 4 der Kartennummer,Karteninhaber,Status der Spesen,"
|
|
45
|
+
"Zahlungsreferenz,Eigene Notiz,Zahlungs-Notiz,Zahlung erfasst am,"
|
|
46
|
+
"Zahlung genehmigt von,Zahlung genehmigt am,Kategorie,Karten Programm,"
|
|
47
|
+
"usedTags,mentions,ultimateCreditorName,ultimateDebitorName\n"
|
|
48
|
+
"e9839ff4-e419-432a-b553-801e13c7e8f1,CREDIT,30.12.2025,"
|
|
49
|
+
"30.12.2025,John Kert,100.00,,100.00,CHF,100.00,John Kert,CH9300762011623852957,"
|
|
50
|
+
"100.00,CHF,,"
|
|
51
|
+
",domestic_iban,,,,f68eb96a17764c4db09912ef65be3b63,,"
|
|
52
|
+
"Subject Test,,,,general,,"
|
|
53
|
+
",,,\n"
|
|
54
|
+
)
|
|
55
|
+
assert importer.identify(str(iban_csv))
|
|
56
|
+
|
|
57
|
+
wrong_iban = Importer(CASH, iban="CH0000000000000000000")
|
|
58
|
+
assert not wrong_iban.identify(str(iban_csv))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_identify_iban_matches_spaceless(tmp_path: Path) -> None:
|
|
62
|
+
importer = Importer(CASH, iban="CH9300762011623852957")
|
|
63
|
+
csv_file = tmp_path / "spaceless.csv"
|
|
64
|
+
csv_file.write_text(
|
|
65
|
+
"Transaktions-ID,Belastung oder Gutschrift,Buchungs-Datum,"
|
|
66
|
+
"Transaktions-Datum,Transaktions-Info,Betrag,Belastung (CHF),"
|
|
67
|
+
"Gutschrift (CHF),Kontowährung,Saldo (CHF),Gegenpartei,IBAN Gegenpartei,"
|
|
68
|
+
"Original Transaktionsbetrag,Transaktionswährung,Devisenkurs,Gebühr,"
|
|
69
|
+
"Zahlungstyp,Letzte 4 der Kartennummer,Karteninhaber,Status der Spesen,"
|
|
70
|
+
"Zahlungsreferenz,Eigene Notiz,Zahlungs-Notiz,Zahlung erfasst am,"
|
|
71
|
+
"Zahlung genehmigt von,Zahlung genehmigt am,Kategorie,Karten Programm,"
|
|
72
|
+
"usedTags,mentions,ultimateCreditorName,ultimateDebitorName\n"
|
|
73
|
+
"e9839ff4-e419-432a-b553-801e13c7e8f1,CREDIT,30.12.2025,"
|
|
74
|
+
"30.12.2025,John Kert,100.00,,100.00,CHF,100.00,John Kert,CH9300762011623852957,"
|
|
75
|
+
"100.00,CHF,,"
|
|
76
|
+
",domestic_iban,,,,f68eb96a17764c4db09912ef65be3b63,,"
|
|
77
|
+
"Subject Test,,,,general,,"
|
|
78
|
+
",,,\n"
|
|
79
|
+
)
|
|
80
|
+
assert importer.identify(str(csv_file))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_identify_iban_ignored_when_none() -> None:
|
|
84
|
+
assert _importer().identify(FIXTURE)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_account() -> None:
|
|
88
|
+
assert _importer().account("ignored") == CASH
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_filename() -> None:
|
|
92
|
+
assert _importer().filename("ignored") == "statement.csv"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_extract_transactions() -> None:
|
|
96
|
+
entries = _importer().extract(FIXTURE, existing=[])
|
|
97
|
+
txns = [e for e in entries if isinstance(e, data.Transaction)]
|
|
98
|
+
assert len(txns) == 3
|
|
99
|
+
|
|
100
|
+
debit = txns[0]
|
|
101
|
+
assert debit.date == Date(2026, 1, 5)
|
|
102
|
+
assert debit.payee == "ACME Hosting Inc"
|
|
103
|
+
assert debit.narration == "Debit card payment"
|
|
104
|
+
assert debit.flag == "!" # no rule matched
|
|
105
|
+
assert len(debit.postings) == 1
|
|
106
|
+
assert debit.postings[0].units is not None
|
|
107
|
+
assert debit.postings[0].units.number == Decimal("-25.50")
|
|
108
|
+
assert debit.meta["bank_ref"] == "e9839ff4-e419-432a-b553-801e13c7e8f1"
|
|
109
|
+
|
|
110
|
+
credit = txns[1]
|
|
111
|
+
assert credit.date == Date(2026, 1, 10)
|
|
112
|
+
assert credit.payee == "Client AG"
|
|
113
|
+
assert credit.flag == "*" # rule matched
|
|
114
|
+
assert [p.account for p in credit.postings] == [CASH, "Assets:Receivable"]
|
|
115
|
+
assert credit.postings[1].units is not None
|
|
116
|
+
assert credit.postings[1].units.number == Decimal("-1000.00")
|
|
117
|
+
|
|
118
|
+
debit2 = txns[2]
|
|
119
|
+
assert debit2.date == Date(2026, 1, 15)
|
|
120
|
+
assert debit2.meta["bank_ref"] == "a1b2c3d4-5678-90ab-cdef-1234567890ab"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_extract_dedup_by_reference() -> None:
|
|
124
|
+
meta = data.new_metadata("ledger.bean", 1)
|
|
125
|
+
meta["bank_ref"] = "e9839ff4-e419-432a-b553-801e13c7e8f1"
|
|
126
|
+
booked = data.Transaction(
|
|
127
|
+
meta,
|
|
128
|
+
Date(2026, 1, 5),
|
|
129
|
+
"*",
|
|
130
|
+
"ACME",
|
|
131
|
+
"already booked",
|
|
132
|
+
data.EMPTY_SET,
|
|
133
|
+
data.EMPTY_SET,
|
|
134
|
+
[],
|
|
135
|
+
)
|
|
136
|
+
entries = _importer().extract(FIXTURE, existing=[booked])
|
|
137
|
+
txns = [e for e in entries if isinstance(e, data.Transaction)]
|
|
138
|
+
assert [t.meta["bank_ref"] for t in txns] == [
|
|
139
|
+
"f68eb96a-1776-4c4d-b099-12ef65be3b63",
|
|
140
|
+
"a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_extract_empty_csv(tmp_path: Path) -> None:
|
|
145
|
+
empty = tmp_path / "empty.csv"
|
|
146
|
+
empty.write_text(
|
|
147
|
+
"Transaktions-ID,Belastung oder Gutschrift,Buchungs-Datum,"
|
|
148
|
+
"Transaktions-Datum,Gegenpartei,Transaktions-Info,"
|
|
149
|
+
"Gutschrift (CHF),Belastung (CHF),Betrag\n"
|
|
150
|
+
)
|
|
151
|
+
entries = _importer().extract(str(empty), existing=[])
|
|
152
|
+
assert not entries
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_betrag_fallback(tmp_path: Path) -> None:
|
|
156
|
+
csv = tmp_path / "betrag.csv"
|
|
157
|
+
csv.write_text(
|
|
158
|
+
"Transaktions-ID,Belastung oder Gutschrift,Buchungs-Datum,"
|
|
159
|
+
"Transaktions-Datum,Gegenpartei,Transaktions-Info,"
|
|
160
|
+
"Gutschrift (CHF),Belastung (CHF),Betrag\n"
|
|
161
|
+
"R1,DEBIT,01.02.2026,01.02.2026,Some Vendor,Payment,,,100.00\n"
|
|
162
|
+
"R2,CREDIT,02.02.2026,02.02.2026,Income,Salary,,,500.00\n"
|
|
163
|
+
)
|
|
164
|
+
entries = _importer().extract(str(csv), existing=[])
|
|
165
|
+
txns = [e for e in entries if isinstance(e, data.Transaction)]
|
|
166
|
+
assert len(txns) == 2
|
|
167
|
+
assert txns[0].postings[0].units is not None
|
|
168
|
+
assert txns[0].postings[0].units.number == Decimal("-100.00")
|
|
169
|
+
assert txns[1].postings[0].units is not None
|
|
170
|
+
assert txns[1].postings[0].units.number == Decimal("500.00")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_payee_rules_draft_counter_leg() -> None:
|
|
174
|
+
importer = Importer(CASH, payee_rules=[(r"client", "Assets:Receivable", "*")])
|
|
175
|
+
entries = importer.extract(FIXTURE, existing=[])
|
|
176
|
+
txns = [e for e in entries if isinstance(e, data.Transaction)]
|
|
177
|
+
matched = [t for t in txns if len(t.postings) == 2]
|
|
178
|
+
assert len(matched) == 1
|
|
179
|
+
assert matched[0].payee == "Client AG"
|
|
180
|
+
assert matched[0].postings[1].account == "Assets:Receivable"
|