Compare commits

...

3 commits

Author SHA1 Message Date
Antoine Martin 524a2e5787 update to new CdE CSV format 2022-10-28 16:38:33 +02:00
Antoine Martin c576c54ed3 flake.lock: Update
Flake lock file updates:

• Updated input 'flake-utils':
    'github:numtide/flake-utils/846b2ae0fc4cc943637d3d1def4454213e203cba' (2022-01-20)
  → 'github:numtide/flake-utils/c0e246b9b83f637f4681389ecabcb2681b4f3af0' (2022-08-07)
• Updated input 'poetry2nix':
    'github:nix-community/poetry2nix/9b601b8cd0f5545ddb0e6a36a01983aca17d9f2a' (2022-01-28)
  → 'github:nix-community/poetry2nix/289efb187123656a116b915206e66852f038720e' (2022-10-28)
• Updated input 'poetry2nix/flake-utils':
    'github:numtide/flake-utils/3982c9903e93927c2164caa727cd3f6a0e6d14cc' (2021-01-07)
  → 'github:numtide/flake-utils/c0e246b9b83f637f4681389ecabcb2681b4f3af0' (2022-08-07)
• Updated input 'poetry2nix/nixpkgs':
    'github:NixOS/nixpkgs/04af07c659c6723a2259bb6bc00a47ec53330f20' (2021-01-15)
  → 'github:NixOS/nixpkgs/38164d1660dcc24b41a5a22f1e9ef075a8e26714' (2022-10-28)
2022-10-28 15:59:11 +02:00
Antoine Martin b05dc8a5df flake: switch to NixOS 22.05 channel 2022-10-28 15:58:31 +02:00
3 changed files with 46 additions and 114 deletions

View file

@ -11,44 +11,20 @@ from beancount.core.number import Decimal # type: ignore
from beancount.ingest import cache, importer # type: ignore from beancount.ingest import cache, importer # type: ignore
INDEX_DATE = 0 COL_DATE = "Date de comptabilisation"
INDEX_TRANSACTION_NUMBER = 1 COL_LABEL = "Libelle operation"
INDEX_LABEL = 2 COL_DEBIT = "Debit"
INDEX_DEBIT = 3 COL_CREDIT = "Credit"
INDEX_CREDIT = 4 COL_DETAIL = "Informations complementaires"
INDEX_DETAIL = 5
END_DATE_REGEX = "Date de fin de téléchargement : ([0-3][0-9]/[0-1][0-9]/[0-9]{4})" END_DATE_REGEX = "Date de fin de téléchargement : ([0-3][0-9]/[0-1][0-9]/[0-9]{4})"
START_DATE_REGEX = "Date de début de téléchargement : ([0-3][0-9]/[0-1][0-9]/[0-9]{4})" START_DATE_REGEX = "Date de début de téléchargement : ([0-3][0-9]/[0-1][0-9]/[0-9]{4})"
EXPECTED_HEADER = "Date de comptabilisation;Libelle simplifie;Libelle operation;Reference;Informations complementaires;Type operation;Categorie;Sous categorie;Debit;Credit;Date operation;Date de valeur;Pointage operation"
def is_valid_header(header: list[str]) -> bool:
return (
header[INDEX_DATE] == "Date"
and header[INDEX_TRANSACTION_NUMBER] == "Numéro d'opération"
and header[INDEX_LABEL] == "Libellé"
and header[INDEX_DEBIT] == "Débit"
and header[INDEX_CREDIT] == "Crédit"
and header[INDEX_DETAIL] == "Détail"
)
def get_date(file: cache._FileMemo, regex: str) -> Optional[date]: def is_valid_header(header: str) -> bool:
match: Optional[re.Match] = re.search(regex, file.head()) return header == EXPECTED_HEADER
if match is None:
return None
date_str: Optional[str] = match.group(1)
if date_str is None:
return None
return datetime.strptime(date_str, "%d/%m/%Y").date()
def get_end_date(file: cache._FileMemo) -> Optional[date]:
return get_date(file, END_DATE_REGEX)
def get_start_date(file: cache._FileMemo) -> Optional[date]:
return get_date(file, START_DATE_REGEX)
class CDEImporter(importer.ImporterProtocol): class CDEImporter(importer.ImporterProtocol):
@ -60,16 +36,7 @@ class CDEImporter(importer.ImporterProtocol):
# NOTE: beancount.ingest.cache._FileMemo handles automatic encoding # NOTE: beancount.ingest.cache._FileMemo handles automatic encoding
# detection # detection
lines: list[str] = file.head().splitlines() lines: list[str] = file.head().splitlines()
csv_reader = csv.reader( header: str = lines[0]
lines, delimiter=";", strict=True, quoting=csv.QUOTE_NONE
)
# header is actually on the 5th line, the previous ones contain
# miscellaneous information
header: Optional[list[str]] = next(islice(csv_reader, 4, None))
if header is None:
return False
return is_valid_header(header) return is_valid_header(header)
except: except:
@ -82,73 +49,38 @@ class CDEImporter(importer.ImporterProtocol):
return "CaisseEpargne_Statement.csv" return "CaisseEpargne_Statement.csv"
def file_date(self, file: cache._FileMemo) -> Optional[date]: def file_date(self, file: cache._FileMemo) -> Optional[date]:
return get_end_date(file) lines: list[str] = file.contents().splitlines()
csv_reader: csv.DictReader = csv.DictReader(
lines, delimiter=";", strict=True, quoting=csv.QUOTE_NONE
)
row: Optional[dict[str, str]] = next(csv_reader)
if row is None:
return None
return datetime.strptime(
row[COL_DATE], "%d/%m/%Y"
).date()
def extract(self, file: cache._FileMemo, existing_entries=None) -> list[Any]: def extract(self, file: cache._FileMemo, existing_entries=None) -> list[Any]:
directives: list[Any] = [] directives: list[Any] = []
end_date: Optional[date] = get_end_date(file)
start_date: Optional[date] = get_start_date(file)
if end_date is None or start_date is None:
return directives
lines: list[str] = file.contents().splitlines() lines: list[str] = file.contents().splitlines()
csv_reader = csv.reader( csv_reader = csv.DictReader(
lines, delimiter=";", strict=True, quoting=csv.QUOTE_NONE lines, delimiter=";", strict=True, quoting=csv.QUOTE_NONE
) )
# first 3 lines are useless
for _ in range(3):
next(csv_reader)
# 4th line is usually the final balance
row: Optional[list[str]] = next(csv_reader)
if row is None:
return directives
if row[0] == "Solde en fin de période":
meta = data.new_metadata(file.name, 4)
balance = Decimal(row[4].replace(",", "."))
directives.append(
data.Balance(
meta=meta,
date=end_date,
account=self.account,
amount=Amount(balance, "EUR"),
tolerance=None,
diff_amount=None,
)
)
# skip headings
next(csv_reader)
for index, row in enumerate(csv_reader): for index, row in enumerate(csv_reader):
lineno: int = index + 6 # entries start at line 6 lineno: int = index + 2 # entries start at line 2
meta = data.new_metadata(file.name, lineno) meta = data.new_metadata(file.name, lineno)
if row[0] == "Solde en début de période":
balance = Decimal(row[4].replace(",", "."))
directives.append(
data.Balance(
meta=meta,
date=start_date,
account=self.account,
amount=Amount(balance, "EUR"),
tolerance=None,
diff_amount=None,
)
)
# should be the last line anyway
continue
transaction_date: date = datetime.strptime( transaction_date: date = datetime.strptime(
row[INDEX_DATE], "%d/%m/%y" row[COL_DATE], "%d/%m/%Y"
).date() ).date()
label: str = row[INDEX_LABEL] label: str = row[COL_LABEL]
debit: str = row[INDEX_DEBIT] debit: str = row[COL_DEBIT]
credit: str = row[INDEX_CREDIT] credit: str = row[COL_CREDIT]
detail: str = row[COL_DETAIL] if row[COL_DETAIL] else ""
postings: list[data.Posting] = [] postings: list[data.Posting] = []
@ -174,7 +106,7 @@ class CDEImporter(importer.ImporterProtocol):
transaction_date, transaction_date,
self.FLAG, self.FLAG,
label, label,
"", detail,
data.EMPTY_SET, data.EMPTY_SET,
data.EMPTY_SET, data.EMPTY_SET,
postings, postings,

View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1642700792, "lastModified": 1659877975,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -17,11 +17,11 @@
}, },
"flake-utils_2": { "flake-utils_2": {
"locked": { "locked": {
"lastModified": 1610051610, "lastModified": 1659877975,
"narHash": "sha256-U9rPz/usA1/Aohhk7Cmc2gBrEEKRzcW4nwPWMPwja4Y=", "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "3982c9903e93927c2164caa727cd3f6a0e6d14cc", "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -32,27 +32,27 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1643247693, "lastModified": 1666867875,
"narHash": "sha256-rmShxIuNjYBz4l83J0J++sug+MURUY1koPCzX4F8hfo=", "narHash": "sha256-3nD7iQXd/J6KjkT8IjozTuA5p8qjiLKTxvOUmH+AzNM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6c4b9f1a2fd761e2d384ef86cff0d208ca27fdca", "rev": "c132d0837dfb9035701dcd8fc91786c605c855c3",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-21.11", "ref": "nixos-22.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1610729867, "lastModified": 1666963493,
"narHash": "sha256-bk/SBaBLqZX/PEqal27DMQwAHHl0dcZMp8NNksQr80s=", "narHash": "sha256-RbTJWOQmAAge/7HqD5qDvvTV9devzs/lXwwcDKruOcM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "04af07c659c6723a2259bb6bc00a47ec53330f20", "rev": "38164d1660dcc24b41a5a22f1e9ef075a8e26714",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -67,11 +67,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1643339875, "lastModified": 1666918719,
"narHash": "sha256-9mIB+pCvo1xgLfZZzs9R6j0/9pO2dFCsVLiyuyLyAU0=", "narHash": "sha256-BkK42fjAku+2WgCOv2/1NrPa754eQPV7gPBmoKQBWlc=",
"owner": "nix-community", "owner": "nix-community",
"repo": "poetry2nix", "repo": "poetry2nix",
"rev": "9b601b8cd0f5545ddb0e6a36a01983aca17d9f2a", "rev": "289efb187123656a116b915206e66852f038720e",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,6 +1,6 @@
{ {
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-21.11"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
poetry2nix.url = "github:nix-community/poetry2nix"; poetry2nix.url = "github:nix-community/poetry2nix";
}; };