Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(banks): add support for zurcherkantonalbank #167

Merged
merged 1 commit into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/monopoly/banks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .ocbc import Ocbc
from .standard_chartered import StandardChartered
from .uob import Uob
from .zkb import ZurcherKantonalBank

banks: list[Type["BankBase"]] = [
Citibank,
Expand All @@ -21,6 +22,7 @@
Ocbc,
StandardChartered,
Uob,
ZurcherKantonalBank,
]

logger = logging.getLogger(__name__)
Expand Down
3 changes: 3 additions & 0 deletions src/monopoly/banks/zkb/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .zkb import ZurcherKantonalBank

__all__ = ["ZurcherKantonalBank"]
34 changes: 34 additions & 0 deletions src/monopoly/banks/zkb/zkb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
from re import compile as regex

from monopoly.config import StatementConfig
from monopoly.constants import BankNames, DebitTransactionPatterns, EntryType
from monopoly.constants.date import ISO8601
from monopoly.identifiers import MetadataIdentifier

from ..base import BankBase

logger = logging.getLogger(__name__)


class ZurcherKantonalBank(BankBase):
debit_config = StatementConfig(
statement_type=EntryType.DEBIT,
bank_name=BankNames.ZKB,
statement_date_pattern=regex(rf"Balance as of: ({ISO8601.DD_MM_YYYY})"),
header_pattern=regex(r"(Date.*Booking text.*Debit CHF.*Credit CHF)"),
transaction_pattern=DebitTransactionPatterns.ZKB,
multiline_transactions=True,
)

identifiers = [
[
MetadataIdentifier(
format="PDF 1.7",
title="SLK_Vermoegensinfo_Group",
creator="Designer",
producer="PDFlib+PDI",
)
]
]
statement_configs = [debit_config]
3 changes: 3 additions & 0 deletions src/monopoly/constants/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class DateFormats(StrEnum):
class ISO8601(RegexEnum):
DD_MM = rf"\b({DateFormats.DD}[\/\-\s]{DateFormats.MM})"
DD_MM_YY = rf"\b({DateFormats.DD}[\/\-\s]{DateFormats.MM}[\/\-\s]{DateFormats.YY})"
DD_MM_YYYY = (
rf"\b({DateFormats.DD}[\/\-\s.]{DateFormats.MM}[\/\-\s.]{DateFormats.YYYY})"
)
DD_MMM = rf"\b({DateFormats.DD}[-\s]{DateFormats.MMM})"
DD_MMM_RELAXED = DD_MMM.replace(r"[-\s]", r"(?:[-\s]|)")
DD_MMM_YY = rf"\b({DateFormats.DD}[-\s]{DateFormats.MMM}[-\s]{DateFormats.YY})"
Expand Down
8 changes: 8 additions & 0 deletions src/monopoly/constants/statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class BankNames(AutoEnum):
OCBC = auto()
STANDARD_CHARTERED = auto()
UOB = auto()
ZKB = auto()


class InternalBankNames(AutoEnum):
Expand Down Expand Up @@ -167,3 +168,10 @@ class DebitTransactionPatterns(RegexEnum):
+ SharedPatterns.AMOUNT_EXTENDED_WITHOUT_EOL
+ SharedPatterns.BALANCE
)
ZKB = (
rf"(?P<transaction_date>{ISO8601.DD_MM_YYYY})\s+"
+ SharedPatterns.DESCRIPTION
+ r"(?P<amount>\d{1,3}(\'\d{3})*(\.\d+)?)\s+"
+ rf"(?P<value_date>{ISO8601.DD_MM_YYYY})\s+"
+ SharedPatterns.BALANCE
)
3 changes: 2 additions & 1 deletion src/monopoly/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ def convert_date(transaction: Transaction, transaction_date_order: DateOrder):
e.g. if the statement month is Jan/Feb 2024, transactions from
Oct/Nov/Dec should be attributed to the previous year.
"""
transaction.date += f" {statement_date.year}"
if str(statement_date.year) not in transaction.date:
transaction.date += f" {statement_date.year}"
parsed_date = parse(
transaction.date,
settings=transaction_date_order.settings,
Expand Down
15 changes: 12 additions & 3 deletions src/monopoly/statements/debit_statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,27 @@ def get_debit_suffix(self, transaction_match: TransactionMatch) -> str | None:

@lru_cache
def get_withdrawal_pos(self, page_number: int) -> int | None:
return self.get_column_pos("withdraw", page_number=page_number)
common_names = ["withdraw", "debit"]
for name in common_names:
if pos := self.get_column_pos(name, page_number=page_number):
return pos
logger.warning("`withdrawal` column not found in header")
return False

@lru_cache
def get_deposit_pos(self, page_number: int) -> int | None:
return self.get_column_pos("deposit", page_number=page_number)
common_names = ["deposit", "credit"]
for name in common_names:
if pos := self.get_column_pos(name, page_number=page_number):
return pos
logger.warning("`deposit` column not found in header")
return False

@lru_cache
def get_column_pos(self, column_type: str, page_number: int) -> int | None:
pattern = re.compile(rf"{column_type}[\w()$]*", re.IGNORECASE)
if match := pattern.search(self.header):
return self.get_header_pos(match.group(), page_number)
logger.warning(f"`{column_type}` column not found in header")
return None

@lru_cache
Expand Down
6 changes: 4 additions & 2 deletions src/monopoly/statements/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,16 @@ def remove_extra_whitespace(cls, value: str) -> str:
@field_validator(Columns.AMOUNT, mode="before")
def prepare_amount_for_float_coercion(cls, amount: str) -> str:
"""
Replaces commas, whitespaces and parentheses in string representation of floats
Replaces commas, whitespaces, apostrophes and parentheses in string
representation of floats
e.g.
1'234.00 -> 1234.00
1,234.00 -> 1234.00
(-10.00) -> -10.00
(-1.56 ) -> -1.56
"""
if isinstance(amount, str):
return re.sub(r"[,)(\s]", "", amount)
return re.sub(r"[,)(\s']", "", amount)
return amount

# pylint: disable=bad-classmethod-argument
Expand Down
Loading