Skip to content
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: 1 addition & 1 deletion .github/workflows/biome-lint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ jobs:
run: npx biome check ai-invoice-extractor --formatter-enabled=false

- name: Run Biome Formatter Check
run: npx biome check ai-invoice-extractor --linter-enabled=false
run: npx biome check ai-invoice-extractor --linter-enabled=false
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ Thumbs.db
*.csv
*.js
!*.json
package-lock.json
# package-lock.json
package.json
!index_by_mcp.json
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ At its core, Well is a **Chrome extension** that automates browser workflows on
- Google Drive and Slack integrations
- AI-generated workflow blueprints
- Self-healing automations that adapt to changes
- Compatibility also includes connectivity with e-invoicing standards, including Factur-X.
- Multi-format export support (JSON, CSV, XML, UBL, QuickBooks, Xero)
- Built-in validation for invoice data integrity
- Extensible plugin system for custom formats
- Compatibility with e-invoicing standards including Factur-X and UBL 2.1

We believe invoice exchange should follow a universal protocol: instant, standardized, and automated. You shouldn’t have to think about it. With Well, you won’t.

Expand All @@ -60,6 +63,24 @@ We believe invoice exchange should follow a universal protocol: instant, standar
- **Privacy-first by design**
No passwords stored. Fully compliant with GDPR and CCPA.

- **Export Formats**
Well supports exporting invoice data to multiple formats:

| Format | Description | Best For |
|--------|-------------|----------|
| **JSON** | Standard JSON format | APIs, Web Apps |
| **CSV** | Comma-separated values | Spreadsheets, Data Analysis |
| **XML** | Standard XML format | Enterprise Systems |
| **UBL** | Universal Business Language 2.1 | International e-Invoicing |
| **QuickBooks** | IIF format | QuickBooks Desktop |
| **Xero** | Xero-compatible CSV | Xero Accounting |

- **Data Validation**
- Automatic validation of required fields
- Type checking for amounts and dates
- Extensible validation rules
- Clear error messages for data issues

---

## Use Cases
Expand All @@ -83,6 +104,33 @@ We believe invoice exchange should follow a universal protocol: instant, standar

## How It Works

### Basic Usage

```python
from exporters import get_exporter

# Get an exporter instance
exporter = get_exporter('json') # or 'csv', 'xml', 'ubl', 'quickbooks', 'xero'

# Export data
data = {
'invoice_number': 'INV-1234',
'date': '2025-07-29',
'amount': 199.99,
'customer': 'Acme Corp',
# ... other fields
}

exporter.export(data, 'invoice.json')
```

### Required Fields
All invoices must include these required fields:
- `invoice_number` (str): Unique identifier for the invoice
- `date` (str): Invoice date in YYYY-MM-DD format
- `amount` (float): Total invoice amount (must be positive)
- `customer` (str): Name of the customer

1. **Browse our provider gallery**
Visit [wellapp.ai/providers](http://wellapp.ai/providers) to explore thousands of supported portals.

Expand Down Expand Up @@ -111,6 +159,30 @@ We believe invoice exchange should follow a universal protocol: instant, standar

---

## Extending Well

### Adding New Export Formats

1. Create a new Python file in the `exporters` directory
2. Create a class that inherits from `BaseExporter`
3. Implement the `_export` method
4. Add the `@ExporterFactory.register()` decorator

Example:

```python
from .base_exporter import BaseExporter
from .exporter_factory import ExporterFactory

@ExporterFactory.register("myformat")
class MyFormatExporter(BaseExporter):
"""Exports invoice data to MyFormat."""

def _export(self, data: dict, output_path: str) -> None:
with open(output_path, 'w') as f:
f.write(f"MyFormat: {data['invoice_number']}")
```

## Contributing

We welcome contributions from the community. To propose a fix, feature, or improvement:
Expand Down
76 changes: 65 additions & 11 deletions ai-invoice-extractor/.env
Original file line number Diff line number Diff line change
@@ -1,11 +1,65 @@
# OpenAI
# ==================
# EXTRACTOR_VENDOR="openai" # default value
# EXTRACTOR_MODEL="o4-mini" # default value when openai
EXTRACTOR_API_KEY=

# Mistral
# ==================
# EXTRACTOR_VENDOR="mistral"
# EXTRACTOR_MODEL="mistral-small-latest" # default value when mistral
# EXTRACTOR_API_KEY=
# AI Invoice Extractor Environment Variables
# Copy this file to .env and fill in your values

# =============================================================================
# REQUIRED VARIABLES
# =============================================================================

# Your AI provider API key
# Get your API key from your chosen AI provider:
# - OpenAI: https://platform.openai.com/api-keys
# - Mistral: https://console.mistral.ai/api-keys/
# - Anthropic: https://console.anthropic.com/
# - Google: https://aistudio.google.com/app/apikey
# - Ollama: No API key needed (local setup)
EXTRACTOR_API_KEY=your_api_key_here

# =============================================================================
# OPTIONAL VARIABLES
# =============================================================================

# AI vendor to use (default: openai)
# Available options: openai, mistral, anthropic, google, ollama
EXTRACTOR_VENDOR=openai

# AI model to use (optional - will use default for selected vendor)
#
# OpenAI models: o4-mini, gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo, etc.
# Mistral models: mistral-small-latest, pixtral-large-latest, pixtral-12b-2409
# Google models: gemini-1.5-flash, gemini-1.5-pro, gemini-2.0-flash-exp, etc.
# Anthropic models: claude-3-5-sonnet-20241022, claude-3-opus-20240229, etc.
# Ollama models: llama3.2 (or any model you have locally)
EXTRACTOR_MODEL=o4-mini

# Enable debug mode (default: false)
# Set to true to see detailed logs
EXTRACTOR_DEBUG=false

# =============================================================================
# EXAMPLE CONFIGURATIONS
# =============================================================================

# OpenAI Configuration
# EXTRACTOR_VENDOR=openai
# EXTRACTOR_MODEL=o4-mini
# EXTRACTOR_API_KEY=sk-...

# Mistral Configuration
# EXTRACTOR_VENDOR=mistral
# EXTRACTOR_MODEL=mistral-small-latest
# EXTRACTOR_API_KEY=...

# Anthropic Configuration
# EXTRACTOR_VENDOR=anthropic
# EXTRACTOR_MODEL=claude-3-5-sonnet-20241022
# EXTRACTOR_API_KEY=sk-ant-...

# Google Configuration
# EXTRACTOR_VENDOR=google
# EXTRACTOR_MODEL=gemini-1.5-flash
# EXTRACTOR_API_KEY=...

# Ollama Configuration (Local)
# EXTRACTOR_VENDOR=ollama
# EXTRACTOR_MODEL=llama3.2
# EXTRACTOR_API_KEY= # No API key needed for local Ollama
18 changes: 18 additions & 0 deletions ai-invoice-extractor/exporters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .exporter_factory import get_exporter
from .json_exporter import JSONExporter
from .csv_exporter import CSVExporter
from .xml_exporter import XMLExporter
from .ubl_exporter import UBLExporter
from .quickbooks_exporter import QuickBooksExporter
from .xero_exporter import XeroExporter

__all__ = [
'get_exporter',
'BaseExporter',
'JSONExporter',
'CSVExporter',
'XMLExporter',
'UBLExporter',
'QuickBooksExporter',
'XeroExporter'
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
21 changes: 21 additions & 0 deletions ai-invoice-extractor/exporters/base_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from typing import Dict, Any

class BaseExporter(ABC):
REQUIRED_FIELDS = ['invoice_number', 'date', 'amount', 'customer']

def validate(self, data: Dict[str, Any]) -> None:
missing = [field for field in self.REQUIRED_FIELDS if field not in data]
if missing:
raise ValueError(f"Missing required fields: {', '.join(missing)}")

if not isinstance(data.get('amount'), (int, float)) or data['amount'] < 0:
raise ValueError("Amount must be a positive number")

def export(self, data: Dict[str, Any], output_path: str) -> None:
self.validate(data)
self._export(data, output_path)

@abstractmethod
def _export(self, data: Dict[str, Any], output_path: str) -> None:
pass
11 changes: 11 additions & 0 deletions ai-invoice-extractor/exporters/csv_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import csv
from .base_exporter import BaseExporter
from .exporter_factory import ExporterFactory

@ExporterFactory.register("csv")
class CSVExporter(BaseExporter):
def _export(self, data: dict, output_path: str):
with open(output_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(data.keys())
writer.writerow(data.values())
24 changes: 24 additions & 0 deletions ai-invoice-extractor/exporters/exporter_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# exporter_factory.py
from typing import Dict, Type

class ExporterFactory:
_exporters: Dict[str, Type['BaseExporter']] = {}

@classmethod
def register(cls, name: str) -> callable:
"""Decorator to register exporter classes."""
def wrapper(exporter_cls: Type['BaseExporter']) -> Type['BaseExporter']:
cls._exporters[name.lower()] = exporter_cls
return exporter_cls
return wrapper

@classmethod
def get_exporter(cls, format_name: str) -> 'BaseExporter':
"""Get an exporter instance by format name."""
exporter_cls = cls._exporters.get(format_name.lower())
if not exporter_cls:
raise ValueError(f"Unsupported export format: {format_name}")
return exporter_cls()

# For backward compatibility
get_exporter = ExporterFactory.get_exporter
9 changes: 9 additions & 0 deletions ai-invoice-extractor/exporters/json_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import json
from .base_exporter import BaseExporter
from .exporter_factory import ExporterFactory

@ExporterFactory.register("json")
class JSONExporter(BaseExporter):
def _export(self, data: dict, output_path: str):
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
10 changes: 10 additions & 0 deletions ai-invoice-extractor/exporters/quickbooks_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .base_exporter import BaseExporter
from .exporter_factory import ExporterFactory

@ExporterFactory.register("quickbooks")
class QuickBooksExporter(BaseExporter):
def _export(self, data: dict, output_path: str):
with open(output_path, 'w') as f:
f.write("!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\tNAME\n")
f.write(f"TRNS\tINVOICE\t{data.get('date')}\tAccounts Receivable\t{data.get('amount')}\t{data.get('customer', 'Client')}\n")
f.write("ENDTRNS\n")
13 changes: 13 additions & 0 deletions ai-invoice-extractor/exporters/ubl_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import xml.etree.ElementTree as ET
from .base_exporter import BaseExporter
from .exporter_factory import ExporterFactory

@ExporterFactory.register("ubl")
class UBLExporter(BaseExporter):
def _export(self, data: dict, output_path: str):
invoice = ET.Element("Invoice", xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2")
ET.SubElement(invoice, "ID").text = data.get("invoice_number", "INV-001")
ET.SubElement(invoice, "IssueDate").text = data.get("date", "2025-01-01")
ET.SubElement(invoice, "LegalMonetaryTotal").text = str(data.get("amount", 0))
tree = ET.ElementTree(invoice)
tree.write(output_path, encoding="utf-8", xml_declaration=True)
18 changes: 18 additions & 0 deletions ai-invoice-extractor/exporters/xero_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import csv
from .base_exporter import BaseExporter
from .exporter_factory import ExporterFactory

@ExporterFactory.register("xero")
class XeroExporter(BaseExporter):
def _export(self, data: dict, output_path: str):
fields = ["InvoiceNumber", "Date", "DueDate", "Amount"]
row = [
data.get("invoice_number", "INV-001"),
data.get("date", "2025-01-01"),
data.get("due_date", "2025-01-31"),
data.get("amount", 0)
]
with open(output_path, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(fields)
writer.writerow(row)
12 changes: 12 additions & 0 deletions ai-invoice-extractor/exporters/xml_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import xml.etree.ElementTree as ET
from .base_exporter import BaseExporter
from .exporter_factory import ExporterFactory

@ExporterFactory.register("xml")
class XMLExporter(BaseExporter):
def _export(self, data: dict, output_path: str):
root = ET.Element("Invoice")
for key, value in data.items():
ET.SubElement(root, key).text = str(value)
tree = ET.ElementTree(root)
tree.write(output_path, encoding="utf-8", xml_declaration=True)
3 changes: 3 additions & 0 deletions ai-invoice-extractor/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"date": "2025-01-01"
}
40 changes: 40 additions & 0 deletions ai-invoice-extractor/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"invoice_id": "INV-1001",
"date": "2025-07-27",
"due_date": "2025-08-15",
"amount": 199.99,
"currency": "USD",
"tax_amount": 39.99,
"discount": 10.0,
"notes": "Thank you for your business!",
"terms": "Net 30",
"vendor": {
"name": "Vendor Inc.",
"tax_id": "123456789",
"address": "123 Business St, City, Country",
"email": "[email protected]",
"phone": "+1 (555) 123-4567"
},
"customer": {
"name": "Acme Corp",
"tax_id": "987654321",
"address": "456 Client Ave, Town, Country",
"email": "[email protected]",
"phone": "+1 (555) 987-6543"
},
"line_items": [
{
"description": "Consulting services",
"quantity": 10,
"unit_price": 19.999,
"total": 199.99,
"tax_rate": 20.0,
"tax_amount": 39.99,
"discount": 10.0
}
],
"payment_terms": "Net 30",
"payment_status": "unpaid",
"shipping_address": "456 Client Ave, Town, Country",
"billing_address": "456 Client Ave, Town, Country"
}
Loading
Loading