ebia 0.1.4__py3-none-any.whl

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.
ebia/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .parser import extract_invoice_fields
2
+
3
+ __all__ = ["extract_invoice_fields"]
4
+ __version__ = "0.1.0"
ebia/cli.py ADDED
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from .parser import extract_invoice_fields
7
+ from .xls_generator import (
8
+ generate_xlsx_by_month_day,
9
+ generate_xlsx_single,
10
+ )
11
+
12
+
13
+ def main(argv=None) -> int:
14
+ ap = argparse.ArgumentParser(prog="ebia")
15
+
16
+ ap.add_argument("--path", required=True, help="PDF file path OR a folder containing PDFs")
17
+ ap.add_argument(
18
+ "--out",
19
+ default=None,
20
+ help=(
21
+ "Single-PDF mode: output .xlsx file path (default: output.xlsx). "
22
+ "Folder mode: output directory where one YYYY-MM.xlsx is created per month "
23
+ "(default: current directory)."
24
+ ),
25
+ )
26
+
27
+ ap.add_argument("--tva", type=float, default=0.20, help="TVA rate (default 0.20)")
28
+
29
+ # Incrémentation pour dossier
30
+ ap.add_argument("--start-piece", type=int, default=1, help="Starting Pièce number (default 1)")
31
+ ap.add_argument(
32
+ "--start-document", type=int, default=1, help="Starting Document number (default 1)"
33
+ )
34
+ ap.add_argument("--piece-width", type=int, default=4, help="Pièce zero-fill width (default 4)")
35
+ ap.add_argument(
36
+ "--document-width", type=int, default=5, help="Document zero-fill width (default 5)"
37
+ )
38
+
39
+ # Pour un seul PDF (optionnel : override)
40
+ ap.add_argument("--piece", default=None, help="Override Pièce for single PDF (e.g. 0000)")
41
+ ap.add_argument(
42
+ "--document", default=None, help="Override Document for single PDF (e.g. 00000)"
43
+ )
44
+
45
+ ap.add_argument("--no-headers", action="store_true", help="Generate Excel without header row")
46
+ ap.add_argument(
47
+ "--recursive", action="store_true", help="Scan subfolders for PDFs (folder mode)"
48
+ )
49
+
50
+ args = ap.parse_args(argv)
51
+
52
+ p = Path(args.path)
53
+ include_headers = not args.no_headers
54
+
55
+ if p.is_file():
56
+ # Single PDF mode
57
+ data = extract_invoice_fields(str(p))
58
+
59
+ out_file = args.out if args.out is not None else "output.xlsx"
60
+
61
+ # piece/document: si non fournis, on utilise start_* avec widths
62
+ if args.piece is None:
63
+ piece = str(args.start_piece).zfill(args.piece_width)
64
+ else:
65
+ piece = args.piece
66
+
67
+ if args.document is None:
68
+ document = str(args.start_document).zfill(args.document_width)
69
+ else:
70
+ document = args.document
71
+
72
+ generate_xlsx_single(
73
+ data,
74
+ out_file,
75
+ piece=piece,
76
+ document=document,
77
+ tva_rate=args.tva,
78
+ include_headers=include_headers,
79
+ )
80
+ print(f"OK (single) -> {out_file}")
81
+ return 0
82
+
83
+ if p.is_dir():
84
+ # Folder mode
85
+ if args.recursive:
86
+ pdfs = sorted(p.rglob("*.pdf")) + sorted(p.rglob("*.PDF"))
87
+ else:
88
+ pdfs = sorted(p.glob("*.pdf")) + sorted(p.glob("*.PDF"))
89
+
90
+ if not pdfs:
91
+ raise SystemExit(f"No PDF files found in folder: {p}")
92
+
93
+ invoice_dicts = []
94
+ skipped = 0
95
+
96
+ for pdf in pdfs:
97
+ try:
98
+ invoice_dicts.append(extract_invoice_fields(str(pdf)))
99
+ except Exception as e:
100
+ skipped += 1
101
+ print(f"[SKIP] {pdf}: {e}")
102
+
103
+ out_dir = args.out if args.out is not None else "./reports"
104
+
105
+ generated = generate_xlsx_by_month_day(
106
+ invoice_dicts,
107
+ out_dir,
108
+ start_piece=args.start_piece,
109
+ start_document=args.start_document,
110
+ piece_width=args.piece_width,
111
+ document_width=args.document_width,
112
+ tva_rate=args.tva,
113
+ include_headers=include_headers,
114
+ )
115
+
116
+ print(f"OK (folder) | parsed={len(invoice_dicts)} skipped={skipped}")
117
+ for f in generated:
118
+ print(f" -> {f}")
119
+ return 0
120
+
121
+ raise SystemExit(f"Invalid path: {p}")
122
+
123
+
124
+ if __name__ == "__main__":
125
+ raise SystemExit(main())
ebia/parser.py ADDED
@@ -0,0 +1,115 @@
1
+ import re
2
+ from datetime import datetime
3
+
4
+ import pdfplumber
5
+
6
+ MONTHS_FR = {
7
+ "janvier": 1,
8
+ "février": 2,
9
+ "fevrier": 2,
10
+ "mars": 3,
11
+ "avril": 4,
12
+ "mai": 5,
13
+ "juin": 6,
14
+ "juillet": 7,
15
+ "août": 8,
16
+ "aout": 8,
17
+ "septembre": 9,
18
+ "octobre": 10,
19
+ "novembre": 11,
20
+ "décembre": 12,
21
+ "decembre": 12,
22
+ }
23
+
24
+ __all__ = ["extract_invoice_fields"]
25
+
26
+
27
+ def normalize(value: str) -> float:
28
+ value = (
29
+ value.replace("\u00a0", " ") # NBSP → space
30
+ .replace("\u202f", " ") # narrow no-break space → space
31
+ .replace("\n", "") # PDF line-wrap artefact
32
+ .replace(" ", "") # remove thousand-separator spaces
33
+ .replace(",", ".")
34
+ ) # French decimal comma → point
35
+ return float(value)
36
+
37
+
38
+ def parse_french_date(date_str: str) -> str | None:
39
+ s = date_str.lower()
40
+ s = (
41
+ s.replace("é", "e")
42
+ .replace("è", "e")
43
+ .replace("ê", "e")
44
+ .replace("à", "a")
45
+ .replace("â", "a")
46
+ .replace("î", "i")
47
+ .replace("ï", "i")
48
+ .replace("ô", "o")
49
+ .replace("û", "u")
50
+ .replace("ù", "u")
51
+ .replace("ç", "c")
52
+ )
53
+
54
+ m = re.search(r"(\d{1,2})\s+([a-z]+)\s+(\d{4})", s)
55
+ if not m:
56
+ return None
57
+
58
+ day, month_name, year = int(m.group(1)), m.group(2), int(m.group(3))
59
+ month = MONTHS_FR.get(month_name)
60
+ if not month:
61
+ return None
62
+
63
+ return datetime(year, month, day).strftime("%Y-%m-%d")
64
+
65
+
66
+ def extract_total_ttc(text: str) -> float | None:
67
+ block = re.search(
68
+ r"Total\s*€\s*HT.*?Total\s*€\s*TTC(.*)", text, flags=re.IGNORECASE | re.DOTALL
69
+ )
70
+ if not block:
71
+ return None
72
+
73
+ zone = block.group(1)
74
+ amounts = re.findall(r"(\d{1,3}(?:[\s\u00A0\u202F]\d{3})*,\d{2})", zone)
75
+ if len(amounts) < 3:
76
+ return None
77
+
78
+ return normalize(amounts[2])
79
+
80
+
81
+ def extract_invoice_fields(pdf_path: str) -> dict:
82
+ with pdfplumber.open(pdf_path) as pdf:
83
+ full_text = "\n".join((p.extract_text() or "") for p in pdf.pages)
84
+
85
+ ref_match = re.search(r"Référence\s*:\s*([^\r\n]+)", full_text, flags=re.IGNORECASE)
86
+ reference_line = ref_match.group(1).strip() if ref_match else None
87
+ reference = None
88
+ if reference_line:
89
+ reference = re.split(
90
+ r"\s+\ble\s+\d{1,2}\s+[A-Za-zÀ-ÿ]+\s+\d{4}\b", reference_line, maxsplit=1
91
+ )[0].strip()
92
+
93
+ date_match = re.search(
94
+ r"\ble\s+(\d{1,2}\s+[A-Za-zÀ-ÿ]+\s+\d{4})", full_text, flags=re.IGNORECASE
95
+ )
96
+ date_iso = parse_french_date(date_match.group(1)) if date_match else None
97
+
98
+ total_ttc = extract_total_ttc(full_text)
99
+
100
+ return {"Client": reference, "date": date_iso, "total_ttc": total_ttc}
101
+
102
+
103
+ def main(argv=None) -> int: # pragma: no cover
104
+ import argparse
105
+
106
+ ap = argparse.ArgumentParser(prog="equipebaie-parse")
107
+ ap.add_argument("pdf", help="Chemin vers la facture PDF")
108
+ args = ap.parse_args(argv)
109
+
110
+ print(extract_invoice_fields(args.pdf))
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__": # pragma: no cover
115
+ raise SystemExit(main())
ebia/xls_generator.py ADDED
@@ -0,0 +1,282 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ from datetime import date, datetime, time
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from openpyxl import Workbook
10
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
11
+ from openpyxl.utils import get_column_letter
12
+
13
+ # En-têtes
14
+ HEADERS = [
15
+ "Statut",
16
+ "Jour",
17
+ "Pièce",
18
+ "Document",
19
+ "Compte général",
20
+ "Compte auxiliaire",
21
+ "Libellé",
22
+ "Débit",
23
+ "Crédit",
24
+ "Date de l'échéance",
25
+ "Documents associés",
26
+ ]
27
+
28
+
29
+ @dataclass
30
+ class Invoice:
31
+ client: str
32
+ date_iso: str # "YYYY-MM-DD"
33
+ total_ttc: float
34
+
35
+
36
+ def _parse_iso_date(s: str) -> date:
37
+ return datetime.strptime(s, "%Y-%m-%d").date()
38
+
39
+
40
+ def _money(x: float) -> float:
41
+ return round(x + 1e-12, 2)
42
+
43
+
44
+ def _zfill_int(n: int, width: int) -> str:
45
+ return str(n).zfill(width)
46
+
47
+
48
+ def invoice_to_rows(
49
+ inv: Invoice,
50
+ *,
51
+ piece: str,
52
+ document: str,
53
+ statut: str = "",
54
+ tva_rate: float = 0.20,
55
+ ) -> list[list[Any]]:
56
+ d = _parse_iso_date(inv.date_iso) # date
57
+ jour_dt = datetime.combine(d, time.min) # datetime (00:00:00)
58
+
59
+ ttc = _money(inv.total_ttc)
60
+ ht = _money(ttc / (1 + tva_rate))
61
+ tva = _money(ttc - ht)
62
+
63
+ # Colonnes: Statut, Jour, Pièce, Document, CG, CA, Libellé, Débit, Crédit, Date éch., Docs assoc.
64
+ return [
65
+ [statut, jour_dt, piece, document, "411", "", inv.client, ttc, "", d, ""],
66
+ [statut, jour_dt, piece, document, "44571", "", inv.client, "", tva, "", ""],
67
+ [statut, jour_dt, piece, document, "701", "", inv.client, "", ht, "", ""],
68
+ ]
69
+
70
+
71
+ def style_as_table(ws) -> None:
72
+ thin = Side(style="thin")
73
+ border = Border(left=thin, right=thin, top=thin, bottom=thin)
74
+
75
+ header_fill = PatternFill("solid", fgColor="E6E6E6")
76
+ header_font = Font(bold=True)
77
+ header_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
78
+
79
+ align_left = Alignment(horizontal="left", vertical="center")
80
+ align_center = Alignment(horizontal="center", vertical="center")
81
+ align_right = Alignment(horizontal="right", vertical="center")
82
+
83
+ max_row = ws.max_row
84
+ max_col = ws.max_column
85
+
86
+ # Header style (ligne 1)
87
+ for c in range(1, max_col + 1):
88
+ cell = ws.cell(row=1, column=c)
89
+ cell.fill = header_fill
90
+ cell.font = header_font
91
+ cell.alignment = header_align
92
+ cell.border = border
93
+
94
+ # Body: bordures + alignement par défaut
95
+ for r in range(2, max_row + 1):
96
+ for c in range(1, max_col + 1):
97
+ cell = ws.cell(row=r, column=c)
98
+ cell.border = border
99
+ cell.alignment = align_left
100
+
101
+ # Alignements par colonne (A..K)
102
+ center_cols = ["A", "B", "C", "D", "E", "F", "J", "K"]
103
+ right_cols = ["H", "I"]
104
+ left_cols = ["G"]
105
+
106
+ for col in center_cols:
107
+ for r in range(2, max_row + 1):
108
+ ws[f"{col}{r}"].alignment = align_center
109
+
110
+ for col in right_cols:
111
+ for r in range(2, max_row + 1):
112
+ ws[f"{col}{r}"].alignment = align_right
113
+
114
+ for col in left_cols:
115
+ for r in range(2, max_row + 1):
116
+ ws[f"{col}{r}"].alignment = align_left
117
+
118
+ # Formats
119
+ # Jour (B) : datetime
120
+ # Date échéance (J) : date
121
+ # Montants (H/I)
122
+ for r in range(2, max_row + 1):
123
+ ws[f"B{r}"].number_format = "dd/mm/yyyy hh:mm:ss"
124
+ ws[f"J{r}"].number_format = "dd/mm/yyyy"
125
+ ws[f"H{r}"].number_format = "#,##0.00"
126
+ ws[f"I{r}"].number_format = "#,##0.00"
127
+
128
+ # Largeurs (ajuste si besoin)
129
+ widths = {
130
+ "A": 10,
131
+ "B": 20,
132
+ "C": 8,
133
+ "D": 10,
134
+ "E": 14,
135
+ "F": 16,
136
+ "G": 40,
137
+ "H": 12,
138
+ "I": 12,
139
+ "J": 16,
140
+ "K": 20,
141
+ }
142
+ for col, w in widths.items():
143
+ ws.column_dimensions[col].width = w
144
+
145
+ # Hauteur header
146
+ ws.row_dimensions[1].height = 20
147
+
148
+ # Freeze + Filter
149
+ ws.freeze_panes = "A2"
150
+ ws.auto_filter.ref = f"A1:{get_column_letter(max_col)}{max_row}"
151
+
152
+
153
+ def write_xlsx_many(
154
+ rows_all: list[list[Any]],
155
+ output_path: str,
156
+ *,
157
+ sheet_name: str = "EBIA",
158
+ include_headers: bool = True,
159
+ ) -> None:
160
+ wb = Workbook()
161
+ ws = wb.active
162
+ ws.title = sheet_name
163
+
164
+ # Header
165
+ if include_headers:
166
+ ws.append(HEADERS)
167
+
168
+ # Data
169
+ for r in rows_all:
170
+ ws.append(r)
171
+
172
+ # Style tableau (APRÈS tout)
173
+ if include_headers and ws.max_row >= 1 and ws.max_column >= 1:
174
+ style_as_table(ws)
175
+
176
+ wb.save(output_path)
177
+
178
+
179
+ def generate_xlsx_single(
180
+ invoice_dict: dict,
181
+ output_path: str,
182
+ *,
183
+ piece: str,
184
+ document: str,
185
+ tva_rate: float = 0.20,
186
+ include_headers: bool = True,
187
+ ) -> None:
188
+ client = (invoice_dict.get("Client") or "").strip()
189
+ date_iso = (invoice_dict.get("date") or "").strip()
190
+ total_ttc = invoice_dict.get("total_ttc")
191
+
192
+ if not client:
193
+ raise ValueError("Missing 'Client'")
194
+ if not date_iso:
195
+ raise ValueError("Missing 'date' (expected YYYY-MM-DD)")
196
+ if total_ttc is None:
197
+ raise ValueError("Missing 'total_ttc'")
198
+
199
+ inv = Invoice(client=client, date_iso=date_iso, total_ttc=float(total_ttc))
200
+ rows = invoice_to_rows(inv, piece=piece, document=document, statut="", tva_rate=tva_rate)
201
+ write_xlsx_many(rows, output_path, include_headers=include_headers)
202
+
203
+
204
+ def generate_xlsx_by_month_day(
205
+ invoice_dicts: list[dict],
206
+ output_dir: str,
207
+ *,
208
+ start_piece: int = 1,
209
+ start_document: int = 1,
210
+ piece_width: int = 4,
211
+ document_width: int = 5,
212
+ tva_rate: float = 0.20,
213
+ include_headers: bool = True,
214
+ ) -> list[str]:
215
+ """Generate one .xlsx per month, with one sheet per day inside each workbook.
216
+
217
+ Invoices are sorted by date before numbering so piece/document counters
218
+ are assigned in chronological order across all months.
219
+
220
+ Returns the list of generated file paths.
221
+ """
222
+ # --- 1. Parse & validate all invoice dicts ---
223
+ valid: list[tuple[Invoice, date]] = []
224
+ for inv_dict in invoice_dicts:
225
+ client = (inv_dict.get("Client") or "").strip()
226
+ date_iso = (inv_dict.get("date") or "").strip()
227
+ total_ttc = inv_dict.get("total_ttc")
228
+ if not client or not date_iso or total_ttc is None:
229
+ continue
230
+ inv = Invoice(client=client, date_iso=date_iso, total_ttc=float(total_ttc))
231
+ valid.append((inv, _parse_iso_date(date_iso)))
232
+
233
+ # Sort chronologically so piece/document numbering follows invoice date order
234
+ valid.sort(key=lambda t: t[1])
235
+
236
+ # --- 2. Assign piece / document numbers (global, sequential) ---
237
+ numbered: list[tuple[Invoice, date, str, str]] = []
238
+ piece_n = start_piece
239
+ doc_n = start_document
240
+ for inv, d in valid:
241
+ piece = _zfill_int(piece_n, piece_width)
242
+ document = _zfill_int(doc_n, document_width)
243
+ numbered.append((inv, d, piece, document))
244
+ piece_n += 1
245
+ doc_n += 1
246
+
247
+ # --- 3. Group by (year, month), then by day ---
248
+ by_month: dict[tuple[int, int], dict[int, list[tuple[Invoice, str, str]]]] = defaultdict(
249
+ lambda: defaultdict(list)
250
+ )
251
+ for inv, d, piece, document in numbered:
252
+ by_month[(d.year, d.month)][d.day].append((inv, piece, document))
253
+
254
+ # --- 4. Write one workbook per month ---
255
+ out_dir = Path(output_dir)
256
+ out_dir.mkdir(parents=True, exist_ok=True)
257
+
258
+ generated: list[str] = []
259
+
260
+ for (year, month), days in sorted(by_month.items()):
261
+ wb = Workbook()
262
+ wb.remove(wb.active) # drop the default empty sheet
263
+
264
+ for day in sorted(days.keys()):
265
+ sheet_name = f"{day:02d}"
266
+ ws = wb.create_sheet(title=sheet_name)
267
+
268
+ if include_headers:
269
+ ws.append(HEADERS)
270
+
271
+ for inv, piece, document in days[day]:
272
+ for row in invoice_to_rows(inv, piece=piece, document=document, tva_rate=tva_rate):
273
+ ws.append(row)
274
+
275
+ if include_headers and ws.max_row >= 1 and ws.max_column >= 1:
276
+ style_as_table(ws)
277
+
278
+ filepath = str(out_dir / f"{year}-{month:02d}.xlsx")
279
+ wb.save(filepath)
280
+ generated.append(filepath)
281
+
282
+ return generated
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: ebia
3
+ Version: 0.1.4
4
+ Summary: EquipeBaie Invoice Automation
5
+ Author: AAH
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pdfplumber
9
+ Requires-Dist: openpyxl
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == "dev"
12
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
13
+ Requires-Dist: pytest-mock>=3.12; extra == "dev"
14
+ Requires-Dist: ruff>=0.4; extra == "dev"
15
+ Requires-Dist: mypy>=1.10; extra == "dev"
16
+
17
+ # EquipeBaie_Freelance-project
18
+ Invoicing software that generates invoices and automatically syncs them to accounting tool.
19
+ # EquipeBaie Invoice Automation (equipebaie-tools)
20
+
21
+ This project aims to build a Python software tool that:
22
+
23
+ 1. Detects new invoices (PDF) in a folder
24
+ 2. Parses invoices and extracts required fields (EquipeBaie requirements)
25
+ 3. Processes/validates the extracted data
26
+ 4. Generates Excel reports (`.xlsx`) classified **by month** and **by week**
27
+
28
+ At the moment, the package contains the first module: **PDF parser**.
29
+
30
+ ---
31
+
32
+ ## Current Status
33
+
34
+ **Step 1 (implemented):** PDF parsing module
35
+ **Next steps:** Excel generation + folder watcher (auto-detect new invoices) + pipeline orchestration
36
+
37
+ ---
38
+
39
+ ## Project Structure (wheel-ready)
40
+
41
+ equipebaie_tools/
42
+ ├─ pyproject.toml
43
+ ├─ README.md
44
+ ├─ src/
45
+ │ └─ equipebaie_tools/
46
+ │ ├─ __init__.py
47
+ │ ├─ parser.py
48
+ │ └─ cli.py
49
+ └─ tests/
50
+ └─ test_import.py
51
+
52
+
53
+
54
+ - `src/equipebaie_tools/` is the installable Python package
55
+ - `parser.py` exposes the main function: `extract_invoice_fields(pdf_path)`
56
+
57
+ ---
58
+
59
+ ## Requirements
60
+
61
+ - Python >= 3.9
62
+ - `pip` up to date
63
+
64
+ Recommended: use a virtual environment.
65
+
66
+ ---
67
+
68
+ ## Installation (Development / Editable)
69
+
70
+
71
+ ```bash
72
+
73
+ ## 1) Create a virtual environment
74
+ **Linux/macOS**
75
+
76
+ python -m venv .venv
77
+ source .venv/bin/activate
78
+
79
+
80
+ Windows (PowerShell)
81
+ python -m venv .venv
82
+ .\.venv\Scripts\Activate.ps1
83
+
84
+ ## 2) Install the package in editable mode
85
+
86
+ pip install -U pip
87
+ pip install -e .
88
+
89
+ ## Quickstart (Build, Install, Run)
90
+
91
+ 1) Clone the repository
92
+
93
+ git clone https://github.com/Alamajdoub9/EquipeBaie_Freelance-project.git
94
+ cd EquipeBaie_Freelance-project
95
+
96
+ 2) Create and activate a virtual environment
97
+ Linux/macOS
98
+
99
+ python3 -m venv .venv
100
+ source .venv/bin/activate
101
+ Windows (PowerShell)
102
+
103
+ python -m venv .venv
104
+ .\.venv\Scripts\Activate.ps1
105
+
106
+ 3) Install build tools and project dependencies
107
+ python -m pip install --upgrade pip
108
+
109
+ python -m pip install build wheel setuptools
110
+
111
+ When you install the wheel (next steps), dependencies are installed automatically.
112
+
113
+ 4) Build the wheel (.whl)
114
+ Run this command from the project root (where pyproject.toml exists):
115
+
116
+ python -m build -w
117
+ After a successful build, you should have a wheel in:
118
+ ls -lh dist/
119
+
120
+ 5) Install the wheel
121
+ pip install --force-reinstall dist/*.whl
122
+
123
+ Solution immédiate (offline)
124
+
125
+ pip install --force-reinstall --no-deps dist/ebia-0.1.0-py3-none-any.whl
126
+
127
+ 6) Run the CLI
128
+ Parse a PDF invoice and print extracted fields:
129
+
130
+ ebia --path facture.pdf
131
+
132
+ ## Generate Excel (XLSX)
133
+
134
+ The CLI `ebia` can generate an Excel file from:
135
+ - a **single PDF invoice**, or
136
+ - a **folder** containing multiple PDF invoices (3 rows per invoice, appended one after another).
137
+
138
+
139
+ ```bash
140
+ 1) Single PDF → one Excel file
141
+ ebia --path "./invoices/facture.pdf" --out "./output/result.xlsx" --piece 0000 --document 00000
142
+
143
+ 2) Folder of PDFs → one consolidated Excel file (auto-increment piece/document)
144
+
145
+ This mode reads all *.pdf files in the folder, parses each invoice, and appends 3 rows per invoice into the same Excel sheet.
146
+
147
+ ebia --path "./invoices" --out "./output/global.xlsx" --start-piece 1 --start-document 1
@@ -0,0 +1,9 @@
1
+ ebia/__init__.py,sha256=qnXyW6dMupNrvl1D0KsnAy3aUhjTfCv_b5AADlaXUxo,103
2
+ ebia/cli.py,sha256=ekBmF8OuZ54QBJHNQBaJSwhyAInRXE5zIbHU2SIoMx4,3914
3
+ ebia/parser.py,sha256=e3wwLezXk1iffc60Ds3NUw3BAHPE0HgcgveY-z4EVS4,3051
4
+ ebia/xls_generator.py,sha256=MI1BbQDj7OEQqm2UfUpCHOsEOpG6O6n0bdVpCTKKyjA,8169
5
+ ebia-0.1.4.dist-info/METADATA,sha256=TCnF8HsFQOURUq-2j55jRcVJb9nHoV0uMwY00af5lB0,3618
6
+ ebia-0.1.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ ebia-0.1.4.dist-info/entry_points.txt,sha256=5jzavokGBKkGnP2rJMEFWEb3YCxQn0Y8nBBh4v9TFtY,39
8
+ ebia-0.1.4.dist-info/top_level.txt,sha256=yg2ZFbd1qylefB1j4xl022tAfrot0s5cSyBF2UrVIeQ,5
9
+ ebia-0.1.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ebia = ebia.cli:main
@@ -0,0 +1 @@
1
+ ebia