diff --git a/.cursorrules b/.cursorrules
deleted file mode 100644
index 4be2d19..0000000
--- a/.cursorrules
+++ /dev/null
@@ -1,194 +0,0 @@
-You are helping to build an AI first Invoice Processing POC which will eventually go into an Accounts Payable Platform - Xelix.
-
-# Persona Information
-
-# AP Processor Context for Xelix Invoice Processing
-
-You are assisting a Senior Accounts Payable Processor using Xelix. They process 200-300 invoices daily, with 40% requiring manual intervention for matching exceptions.
-
-## Critical Mindset:
-- **Match resolution focus**: "Show me WHY this invoice won't match to PO/GR"
-- **Exception patterns**: "Are these all quantity mismatches I can bulk-approve?"
-- **Tolerance awareness**: "Is this $50 variance worth blocking a $50K payment?"
-- **Vendor relationships**: Every matching delay = supplier calling about payment
-
-## Matching Workflow in Xelix:
-
-### 2-Way Match (Invoice ↔ PO):
-```
-Invoice arrives → Find PO → Amount match? → Approve
- ↓ No ↓ No ↓ Wait
- Fuzzy search Price variance? Check tolerance
- Quantity diff? Contact buyer
-```
-
-### 3-Way Match (Invoice ↔ PO ↔ GR):
-```
-Invoice → Match PO → Match GR → All align? → Approve
- ↓ No ↓ No ↓ No ↓
- "Show similar POs" "GR pending?" "Show mismatches"
- "Partial GR?" "Who to contact?"
-```
-
-## Common Matching Exceptions They Face:
-- **Quantity**: "GR shows 95 units received, invoice for 100"
-- **Price**: "PO says $10/unit, invoice shows $10.50"
-- **Partial delivery**: "Multiple GRs for one PO - which ones?"
-- **Unit mismatch**: "PO in cases, invoice in pieces, GR in pallets"
-- **Timing**: "Goods received but GR not yet in system"
-
-## Their Resolution Strategies:
-1. **Quick fixes**: "If variance <$100, approve within tolerance"
-2. **Bulk actions**: "Select all price increases from this vendor"
-3. **Smart routing**: "Quantity issues → Warehouse, Price → Procurement"
-4. **History-based**: "This vendor always ships 5% over - it's accepted"
-
-## Language for Matching Issues:
-- "Short-shipped" = GR quantity < Invoice quantity
-- "Price creep" = Unauthorized price increase on invoice
-- "Split delivery" = One PO, multiple GRs to reconcile
-- "Goods in transit" = Physical receipt without system GR
-- "Tolerance breach" = Variance exceeds auto-approval threshold
-
-## What They Need from Xelix:
-- **Visual clarity**: Highlight WHAT doesn't match (quantity/price/dates)
-- **Smart suggestions**: "3 other invoices have same price increase"
-- **One-click actions**: "Approve all within 2% tolerance"
-- **Clear routing**: "Send to Mike in Warehouse for GR confirmation"
-
-## Hidden Matching Needs:
-- **Pattern memory**: "Remember this vendor always invoices in different UOM"
-- **Proactive alerts**: "5 invoices waiting for same missing GR"
-- **Tolerance templates**: Different rules for different vendors/categories
-- **Match confidence**: "90% sure this is PO 4500123 despite typo"
-
-## Approval Workflow Challenges:
-
-### Approval Routing Logic:
-```
-Exception found → Determine approver → Available? → Approve
- ↓ ↓ No ↓ Wait
- By amount/type/dept Find backup Chase
- Escalate up Remind
-```
-
-### Common Approval Bottlenecks:
-- **Single approver trap**: "Sarah from Procurement is on vacation - 50 invoices stuck"
-- **Circular routing**: "AP → Procurement → Finance → back to AP"
-- **Authority limits**: "$5K needs manager, $25K needs director, now what?"
-- **Missing delegates**: "Who covers for Tom when he's out?"
-- **Approval fatigue**: "200 invoices in John's queue, he's overwhelmed"
-
-## What They Need for Approvals:
-- **Smart routing**: "Route to Amy (backup) since Bob is OOO"
-- **Bulk capabilities**: "John, approve these 30 similar variances at once"
-- **Escalation paths**: "Waiting >48hrs → auto-escalate to manager"
-- **Mobile approval**: "Let Sarah approve urgent items from her phone"
-- **Context in request**: "Approve $500 variance - history shows normal"
-
-## Approval Language:
-- "Sitting with" = Currently in someone's approval queue
-- "Approval chain" = Sequential approvers needed (manager → director)
-- "Delegation matrix" = Who can approve for whom
-- "Auto-escalate" = System pushes to next level after timeout
-- "Approval threshold" = Dollar limit for self-approval
-
-## Their Approval Frustrations:
-1. **Visibility**: "I can't see where this invoice is stuck"
-2. **Reminders**: "I shouldn't manually email chasers all day"
-3. **Delegation**: "System doesn't know Lisa covers for Mark"
-4. **History**: "Why can't approvers see this variance is normal?"
-
-Remember: They're not just matching documents - they're orchestrating complex approval workflows while balancing speed vs. accuracy, maintaining vendor trust while protecting against overpayment. Every matching exception Xelix surfaces should come with a suggested resolution path and clear approval routing.
-
-
-When working inside the backend directory, you are an expert in Python, Django, and scalable web application development.
-
-Key Principles for the backend:
-
- - Write clear, technical responses with precise Django examples.
- - Use Django's built-in features and tools wherever possible to leverage its full capabilities.
- - Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance).
- - Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).
- - Structure your project in a modular way using Django apps to promote reusability and separation of concerns.
-
- Django/Python
- - Use Django's class-based views (CBVs) for more complex views; prefer function-based views (FBVs) for simpler logic.
- - Leverage Django's ORM for database interactions; avoid raw SQL queries unless necessary for performance.
- - Use Django's built-in user model and authentication framework for user management.
- - Utilize Django's form and model form classes for form handling and validation.
- - Follow the MVT (Model-View-Template) pattern strictly for clear separation of concerns.
- - Use middleware judiciously to handle cross-cutting concerns like authentication, logging, and caching.
-
- Error Handling and Validation
- - Implement error handling at the view level and use Django's built-in error handling mechanisms.
- - Use Django's validation framework to validate form and model data.
- - Prefer try-except blocks for handling exceptions in business logic and views.
- - Customize error pages (e.g., 404, 500) to improve user experience and provide helpful information.
- - Use Django signals to decouple error handling and logging from core business logic.
-
- Dependencies
- - Django
- - Django REST Framework (for API development)
-
- Django-Specific Guidelines
- - Use Django templates for rendering HTML and DRF serializers for JSON responses.
- - Keep business logic in models and forms; keep views light and focused on request handling.
- - Use Django's URL dispatcher (urls.py) to define clear and RESTful URL patterns.
- - Apply Django's security best practices (e.g., CSRF protection, SQL injection protection, XSS prevention).
- - Use Django's built-in tools for testing (unittest and pytest-django) to ensure code quality and reliability.
- - Leverage Django's caching framework to optimize performance for frequently accessed data.
- - Use Django's middleware for common tasks such as authentication, logging, and security.
-
- Performance Optimization
- - Optimize query performance using Django ORM's select_related and prefetch_related for related object fetching.
- - Use Django's cache framework with backend support (e.g., Redis or Memcached) to reduce database load.
- - Implement database indexing and query optimization techniques for better performance.
- - Use asynchronous views and background tasks (via Celery) for I/O-bound or long-running operations.
- - Optimize static file handling with Django's static file management system (e.g., WhiteNoise or CDN integration).
-
- Key Conventions
- 1. Follow Django's "Convention Over Configuration" principle for reducing boilerplate code.
- 2. Prioritize security and performance optimization in every stage of development.
- 3. Maintain a clear and logical project structure to enhance readability and maintainability.
-
- Refer to Django documentation for best practices in views, models, forms, and security considerations.
-
-When working inside the frontend directory:
-
-You are a Senior Front-End Developer and an Expert in ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS and modern UI/UX frameworks (e.g., TailwindCSS, Shadcn, Radix). You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning.
-
-- Follow the user's requirements carefully & to the letter.
-- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.
-- Confirm, then write code!
-- Always write correct, best practice, DRY principle (Dont Repeat Yourself), bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines.
-- Focus on easy and readability code, over being performant.
-- Fully implement all requested functionality.
-- Leave NO todo's, placeholders or missing pieces.
-- Ensure code is complete! Verify thoroughly finalised.
-- Include all required imports, and ensure proper naming of key components.
-- Be concise Minimize any other prose.
-- If you think there might not be a correct answer, you say so.
-- If you do not know the answer, say so, instead of guessing.
-
-### Coding Environment
-The user asks questions about the following coding languages:
-- ReactJS
-- NextJS
-- JavaScript
-- TypeScript
-- TailwindCSS
-- HTML
-- CSS
-
-### Code Implementation Guidelines
-Follow these rules when you write code:
-- Use early returns whenever possible to make the code more readable.
-- Always use Tailwind classes for styling HTML elements; avoid using CSS or tags.
-- Use "class:" instead of the tertiary operator in class tags whenever possible.
-- Use descriptive variable and function/const names. Also, event functions should be named with a "handle" prefix, like "handleClick" for onClick and "handleKeyDown" for onKeyDown.
-- Implement accessibility features on elements. For example, a tag should have a tabindex="0", aria-label, on:click, and on:keydown, and similar attributes.
-- Use consts instead of functions, for example, "const toggle = () =>". Also, define a type if possible.
-
-
-Before doing anything, only act when you have 95% confidence in what you are doing. Ask any follow up questions that you may need for clarification until you get this confidence.
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d42acc7..0af3e83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,145 +1,28 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-# Dependencies
-/frontend/node_modules/
-/backend/venv/
-__pycache__/
-*.py[cod]
-*$py.class
+# dependencies
+/node_modules
-# Next.js (Frontend)
-/frontend/.next/
-/frontend/out/
+# next.js
+/.next/
+/out/
-# Production builds
-/frontend/build/
-/backend/staticfiles/
-/dist/
+# production
+/build
-# Debug logs
+# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
-# Environment files (contains sensitive API keys)
+# env files
.env*
-!.env.example
+venv/
-# Vercel
+# vercel
.vercel
-# TypeScript
+# typescript
*.tsbuildinfo
-next-env.d.ts
-
-# Python/Django specific
-*.log
-*.pot
-*.pyc
-*.pyo
-*.pyd
-__pycache__/
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# Django specific
-db.sqlite3
-db.sqlite3-journal
-/backend/media/
-/backend/staticfiles/
-/backend/static/
-*.log
-
-# Virtual environments
-venv/
-ENV/
-env/
-.venv/
-
-# IDE and editor files
-.vscode/
-.idea/
-*.swp
-*.swo
-*~
-
-# OS generated files
-.DS_Store
-.DS_Store?
-._*
-.Spotlight-V100
-.Trashes
-ehthumbs.db
-Thumbs.db
-
-# Temporary files
-temp/
-tmp/
-*.tmp
-*.temp
-
-# Testing
-.coverage
-.pytest_cache/
-.tox/
-.nox/
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-.python-version
-
-# Celery
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
\ No newline at end of file
+next-env.d.ts
\ No newline at end of file
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
deleted file mode 100644
index 76a360d..0000000
--- a/DEPLOYMENT.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# 🚀 Deployment Guide
-
-## Overview
-
-This project consists of:
-
-- **Backend**: Django REST API (deployed to Railway)
-- **Frontend**: Next.js application (deployed to Vercel)
-
-## 🏗️ Backend Deployment (Railway)
-
-### 1. Connect to Railway
-
-1. Go to [Railway](https://railway.app) and sign up/login
-2. Click "New Project" → "Deploy from GitHub repo"
-3. Select your `MikeHiett/invoice-processing-poc` repository
-4. Choose the `django-railway` branch
-
-### 2. Configure Environment Variables
-
-In Railway, go to your project → Variables tab and add:
-
-```bash
-# Django Configuration
-DEBUG=False
-SECRET_KEY=your-super-secret-key-here-make-it-long-and-random
-ALLOWED_HOSTS=your-app-name.railway.app
-
-# AI Service (choose one)
-ANTHROPIC_API_KEY=your-anthropic-api-key
-
-# CORS (will be updated after Vercel deployment)
-CORS_ALLOWED_ORIGINS=http://localhost:3000
-```
-
-### 3. Add PostgreSQL Database
-
-1. In Railway, click "New" → "Database" → "PostgreSQL"
-2. Railway will automatically set the `DATABASE_URL` environment variable
-
-### 4. Deploy Settings
-
-Railway should automatically:
-
-- Detect it's a Python project
-- Use the `Procfile` for deployment commands
-- Run migrations and collect static files
-
-Your backend will be available at: `https://your-app-name.railway.app`
-
-## 🌐 Frontend Deployment (Vercel)
-
-### 1. Connect to Vercel
-
-1. Go to [Vercel](https://vercel.com) and sign up/login
-2. Click "New Project" → Import from GitHub
-3. Select your `MikeHiett/invoice-processing-poc` repository
-4. Set the **Root Directory** to `frontend`
-
-### 2. Configure Environment Variables
-
-In Vercel, go to your project → Settings → Environment Variables:
-
-```bash
-NEXT_PUBLIC_API_URL=https://your-railway-app-name.railway.app
-```
-
-### 3. Deploy
-
-Vercel will automatically deploy your Next.js app.
-
-Your frontend will be available at: `https://your-app-name.vercel.app`
-
-## 🔗 Connect Frontend and Backend
-
-### Update Railway CORS Settings
-
-Once your Vercel app is deployed, update the Railway environment variables:
-
-```bash
-CORS_ALLOWED_ORIGINS=https://your-vercel-app.vercel.app,http://localhost:3000
-ALLOWED_HOSTS=your-railway-app.railway.app,localhost,127.0.0.1
-```
-
-## 🧪 Testing the Deployment
-
-1. Visit your Vercel frontend URL
-2. Go to `/invoices/upload`
-3. Upload a test invoice
-4. Verify it processes correctly
-
-## 🔧 Troubleshooting
-
-### Backend Issues
-
-- Check Railway logs: Railway dashboard → your project → View logs
-- Verify environment variables are set correctly
-- Ensure database is connected
-
-### Frontend Issues
-
-- Check Vercel logs: Vercel dashboard → your project → Functions tab
-- Verify `NEXT_PUBLIC_API_URL` is set correctly
-- Check browser console for errors
-
-### CORS Issues
-
-- Ensure your Vercel domain is in `CORS_ALLOWED_ORIGINS`
-- Verify the backend URL in frontend env vars
-
-## 📝 Environment Variables Summary
-
-### Railway (Backend)
-
-```bash
-DEBUG=False
-SECRET_KEY=your-secret-key
-ALLOWED_HOSTS=your-railway-app.railway.app
-ANTHROPIC_API_KEY=your-api-key
-CORS_ALLOWED_ORIGINS=https://your-vercel-app.vercel.app,http://localhost:3000
-```
-
-### Vercel (Frontend)
-
-```bash
-NEXT_PUBLIC_API_URL=https://your-railway-app.railway.app
-```
-
-## 🎉 Success!
-
-Once deployed, you'll have:
-
-- ✅ Django API running on Railway with PostgreSQL
-- ✅ Next.js frontend on Vercel
-- ✅ AI-powered invoice extraction
-- ✅ Automatic deployments on git push
-
-Your invoice processing app is now live! 🚀
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 9a2a1d8..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,42 +0,0 @@
-# Use Python 3.11
-FROM python:3.11-slim
-
-# Set environment variables
-ENV PYTHONUNBUFFERED=1
-ENV PYTHONDONTWRITEBYTECODE=1
-
-# Set work directory
-WORKDIR /app
-
-# Install system dependencies including OpenCV requirements
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
- gcc \
- pkg-config \
- libpq-dev \
- libgl1-mesa-glx \
- libglib2.0-0 \
- libgomp1 \
- libglib2.0-0 \
- && rm -rf /var/lib/apt/lists/*
-
-# Copy requirements and install Python dependencies
-COPY backend/requirements.txt /app/backend/
-RUN pip install --no-cache-dir -r backend/requirements.txt
-
-# Copy project files
-COPY . /app/
-
-# Create staticfiles directory
-RUN mkdir -p /app/backend/staticfiles
-
-# Expose port
-EXPOSE $PORT
-
-# Command to run the application
-# Migrations, load fixtures, collectstatic, then start server
-CMD cd backend && \
- python manage.py migrate && \
- python manage.py load_csv_data && \
- python manage.py collectstatic --noinput && \
- gunicorn invoice_backend.wsgi:application --bind 0.0.0.0:$PORT
\ No newline at end of file
diff --git a/Procfile b/Procfile
deleted file mode 100644
index c8bb45b..0000000
--- a/Procfile
+++ /dev/null
@@ -1,2 +0,0 @@
-web: cd backend && python manage.py migrate && python manage.py load_csv_data && python manage.py collectstatic --noinput && gunicorn invoice_backend.wsgi:application --bind 0.0.0.0:$PORT
-release: cd backend && python manage.py migrate && python manage.py load_csv_data
diff --git a/README.md b/README.md
index 36845c0..b9190ba 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,11 @@ This proof of concept application demonstrates a modern approach to invoice proc
- AI-powered invoice data extraction
- Clean, responsive interface for managing invoices
-- Seamless integration between Next.js frontend and Django backend
+- Seamless integration between Next.js frontend and Python backend
## Architecture
-The project follows a modern full-stack architecture:
+The project follows a hybrid architecture:
### Frontend
@@ -23,12 +23,10 @@ The project follows a modern full-stack architecture:
### Backend
-- **Django**: Python web framework with Django REST Framework
-- **AI Engineering**: Modular AI services for invoice extraction
- - **Anthropic Claude**: AI models for extracting data from invoice images
- - **AWS Bedrock**: Alternative AI service
+- **FastAPI**: Python-based API server for invoice extraction
+- **OpenAI (Claude)**: AI models for extracting data from invoice images
- **PyMuPDF**: PDF processing
-- **PostgreSQL/SQLite**: Database for storing invoices and extraction jobs
+- **AWS Bedrock**: Alternative AI service
## Features
@@ -37,14 +35,12 @@ The project follows a modern full-stack architecture:
- Preview invoice files
- Create/edit invoices
- Dashboard for invoice management
-- Purchase order and goods received tracking
-- Invoice validation and approval workflow
## Getting Started
### Prerequisites
-- Node.js 18+ and npm/pnpm
+- Node.js 18+ and pnpm
- Python 3.11+
- API keys for Claude/Anthropic or AWS (optional for demo mode)
@@ -60,37 +56,21 @@ The project follows a modern full-stack architecture:
2. Install frontend dependencies:
```bash
- cd frontend
- npm install
- # or
pnpm install
```
-3. Set up backend:
+3. Install backend dependencies:
```bash
- cd ../backend
- python -m venv venv
- source venv/bin/activate # On Windows: venv\Scripts\activate
+ cd backend
pip install -r requirements.txt
```
-4. Set up Django:
-
- ```bash
- python manage.py migrate
- python manage.py collectstatic --noinput
- python manage.py createsuperuser # Optional
- ```
-
-5. Set up environment variables:
- - Create a `.env` file in the backend directory
+4. Set up environment variables:
+ - Create a `.env.local` file in the root directory
- Add your API keys (optional):
```
- SECRET_KEY=your_django_secret_key
ANTHROPIC_API_KEY=your_api_key_here
- AWS_ACCESS_KEY_ID=your_aws_key
- AWS_SECRET_ACCESS_KEY=your_aws_secret
```
### Running the Application
@@ -104,52 +84,32 @@ chmod +x start-dev.sh
This will start:
-- Django backend on port 8000
+- FastAPI backend on port 8000
- Next.js frontend on port 3000
-### Manual Development
-
-Alternatively, you can run each part manually:
-
-```bash
-# Terminal 1 - Backend
-cd backend
-source venv/bin/activate
-python manage.py runserver
-
-# Terminal 2 - Frontend
-cd frontend
-npm run dev
-```
-
## Development
### Project Structure
```
invoice-processing-poc/
-├── frontend/ # Next.js frontend
-│ ├── app/ # Next.js app directory
-│ ├── components/ # React components
-│ ├── public/ # Static assets
-│ ├── package.json # Frontend dependencies
+├── app/ # Next.js app directory
+│ ├── api/ # Next.js API routes
+│ ├── invoices/ # Invoice pages
+│ └── ...
+├── backend/ # Python FastAPI backend
+│ ├── api.py # Main API endpoints
+│ ├── anthropic_client.py # AI service integration
│ └── ...
-├── backend/ # Django backend
-│ ├── ai_engineering/ # AI services and extraction logic
-│ ├── invoices/ # Invoice management app
-│ ├── invoice_extraction/ # AI extraction job management
-│ ├── purchase_orders/ # Purchase order management
-│ ├── goods_received/ # Goods received management
-│ └── manage.py # Django management
-├── start-dev.sh # Development startup script
-└── README.md
+├── components/ # React components
+├── public/ # Static assets
+└── ...
```
### Adding New Features
-1. Backend changes: Add Django apps, models, views, and API endpoints
-2. AI changes: Modify modules in `backend/ai_engineering/`
-3. Frontend changes: Modify components or add new pages in the `frontend/app/` directory
+1. Backend changes: Add endpoints in `backend/api.py`
+2. Frontend changes: Modify components or add new pages in the `app` directory
## License
diff --git a/frontend/app/api/extract-invoice/route.ts b/app/api/extract-invoice/route.ts
similarity index 54%
rename from frontend/app/api/extract-invoice/route.ts
rename to app/api/extract-invoice/route.ts
index 7672aa1..8c9da16 100644
--- a/frontend/app/api/extract-invoice/route.ts
+++ b/app/api/extract-invoice/route.ts
@@ -1,49 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';
// Ensure we're using the full URL with protocol
-const PYTHON_API_URL = process.env.NODE_ENV === 'development'
- ? (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000')
- : (process.env.NEXT_PUBLIC_API_URL || 'https://invoice-processing-poc-production.up.railway.app');
+const PYTHON_API_URL = process.env.PYTHON_API_URL || 'http://localhost:8000';
export async function POST(request: NextRequest) {
try {
- console.log(`Attempting to connect to Python API at: ${PYTHON_API_URL}/api/extract-invoice/`);
+ console.log(`Attempting to connect to Python API at: ${PYTHON_API_URL}/extract-invoice`);
- // Forward the request to the Python Django server
+ // Forward the request to the Python FastAPI server
const formData = await request.formData();
console.log(`Request form data keys: ${Array.from(formData.keys()).join(', ')}`);
- const response = await fetch(`${PYTHON_API_URL}/api/extract-invoice/`, {
+ const response = await fetch(`${PYTHON_API_URL}/extract-invoice`, {
method: 'POST',
body: formData,
// Add a timeout to avoid hanging indefinitely
signal: AbortSignal.timeout(10000) // 10 second timeout
});
- // Check if response is ok and log more details
- console.log(`Response status: ${response.status}`);
- console.log(`Response headers:`, Object.fromEntries(response.headers.entries()));
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error(`Error response from Django API: ${errorText}`);
- throw new Error(`Django API returned ${response.status}: ${errorText}`);
- }
-
// Get the response data
const data = await response.json();
console.log('Successfully received response from Python API');
- // Transform the Django response to match frontend expectations
- const transformedData = {
- document_type: "invoice",
- invoices: data.extracted_invoices || []
- };
-
- console.log('Transformed data:', transformedData);
-
- // Return the transformed response
- return NextResponse.json(transformedData, { status: response.status });
+ // Return the response from the Python API
+ return NextResponse.json(data, { status: response.status });
} catch (error: any) {
console.error('Error proxying to Python API:', error);
diff --git a/app/api/goods-received/route.ts b/app/api/goods-received/route.ts
new file mode 100644
index 0000000..490dd5a
--- /dev/null
+++ b/app/api/goods-received/route.ts
@@ -0,0 +1,12 @@
+import { getGoodsReceived } from "@/lib/data-utils"
+import { NextResponse } from "next/server"
+
+export async function GET() {
+ try {
+ const goodsReceived = await getGoodsReceived()
+ return NextResponse.json(goodsReceived)
+ } catch (error) {
+ console.error("Error fetching goods received:", error)
+ return NextResponse.json({ error: "Failed to fetch goods received" }, { status: 500 })
+ }
+}
diff --git a/app/api/invoices/route.ts b/app/api/invoices/route.ts
new file mode 100644
index 0000000..d1c9c29
--- /dev/null
+++ b/app/api/invoices/route.ts
@@ -0,0 +1,12 @@
+import { getInvoices } from "@/lib/data-utils"
+import { NextResponse } from "next/server"
+
+export async function GET() {
+ try {
+ const invoices = await getInvoices()
+ return NextResponse.json(invoices)
+ } catch (error) {
+ console.error("Error fetching invoices:", error)
+ return NextResponse.json({ error: "Failed to fetch invoices" }, { status: 500 })
+ }
+}
diff --git a/app/api/purchase-orders/route.ts b/app/api/purchase-orders/route.ts
new file mode 100644
index 0000000..7d42198
--- /dev/null
+++ b/app/api/purchase-orders/route.ts
@@ -0,0 +1,12 @@
+import { getPurchaseOrders } from "@/lib/data-utils"
+import { NextResponse } from "next/server"
+
+export async function GET() {
+ try {
+ const purchaseOrders = await getPurchaseOrders()
+ return NextResponse.json(purchaseOrders)
+ } catch (error) {
+ console.error("Error fetching purchase orders:", error)
+ return NextResponse.json({ error: "Failed to fetch purchase orders" }, { status: 500 })
+ }
+}
diff --git a/frontend/app/globals.css b/app/globals.css
similarity index 100%
rename from frontend/app/globals.css
rename to app/globals.css
diff --git a/frontend/app/goods-received/loading.tsx b/app/goods-received/loading.tsx
similarity index 100%
rename from frontend/app/goods-received/loading.tsx
rename to app/goods-received/loading.tsx
diff --git a/app/goods-received/page.tsx b/app/goods-received/page.tsx
new file mode 100644
index 0000000..7bfe41d
--- /dev/null
+++ b/app/goods-received/page.tsx
@@ -0,0 +1,81 @@
+import { Search, SlidersHorizontal, Bell } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import Sidebar from "@/components/sidebar"
+import GoodsReceivedTable from "@/components/goods-received-table"
+
+export default function GoodsReceivedPage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Goods Received Notes
+
+
+
+
+
+
+
+
+
+
+
+
+ All
+
+ Complete
+ Partial
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/app/invoice-extraction/page.tsx b/app/invoice-extraction/page.tsx
similarity index 100%
rename from frontend/app/invoice-extraction/page.tsx
rename to app/invoice-extraction/page.tsx
diff --git a/frontend/app/invoices/[id]/loading.tsx b/app/invoices/[id]/loading.tsx
similarity index 100%
rename from frontend/app/invoices/[id]/loading.tsx
rename to app/invoices/[id]/loading.tsx
diff --git a/frontend/app/invoices/[id]/page.tsx b/app/invoices/[id]/page.tsx
similarity index 84%
rename from frontend/app/invoices/[id]/page.tsx
rename to app/invoices/[id]/page.tsx
index 86392f1..b6625f2 100644
--- a/frontend/app/invoices/[id]/page.tsx
+++ b/app/invoices/[id]/page.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState, useEffect } from "react"
+import { useState } from "react"
import { ArrowLeft, Search, Maximize, FileText, Circle, Edit, Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
@@ -11,30 +11,11 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
export default function InvoiceDetailsPage({ params }: { params: { id: string } }) {
const [zoomLevel, setZoomLevel] = useState(100)
- const [fileData, setFileData] = useState(null)
- const [fileType, setFileType] = useState(null)
- const [fileName, setFileName] = useState(null)
-
- // Mock data - in production this would come from your API
- useEffect(() => {
- // Simulate loading invoice data including file information
- // In production, fetch from API endpoint like /api/invoices/${params.id}
-
- // For demo purposes, let's show an image of an invoice
- // In production, this would be determined by the actual file type
- setFileType('image/png')
- setFileName('invoice-sample.png')
-
- // Using a local sample invoice image for demonstration
- // In production, this would be the actual file URL from your backend
- // For example: setFileData(`${API_URL}/media/invoice_uploads/${invoiceFile}`)
- setFileData('/sample-invoice.png')
- }, [params.id])
return (
-
+ {invoice?.po_number ?
+ `PO Number: ${invoice.po_number}` :
+ "No purchase orders linked to this invoice. Click \"Add PO\" to link a purchase order."}
+
+ No line items found. Click "Add Item" to add line items to this invoice.
+
+ )}
+
+
+
+
+
+
+
+
Activity Log
+
+
+
+
+
+
+
Invoice created
+
Created from uploaded file with extracted data
+
Just now
+
+
+
+
+
+
+
+
+
+
Attachments
+
+
+
+ {fileName ? (
+
+
+
+
+
+
{fileName}
+
Uploaded just now
+
+
+
+
+
+ ) : (
+
+
No attachments yet.
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/frontend/app/invoices/upload/loading.tsx b/app/invoices/upload/loading.tsx
similarity index 100%
rename from frontend/app/invoices/upload/loading.tsx
rename to app/invoices/upload/loading.tsx
diff --git a/frontend/app/invoices/upload/page.tsx b/app/invoices/upload/page.tsx
similarity index 91%
rename from frontend/app/invoices/upload/page.tsx
rename to app/invoices/upload/page.tsx
index 552b7c5..f69129c 100644
--- a/frontend/app/invoices/upload/page.tsx
+++ b/app/invoices/upload/page.tsx
@@ -62,8 +62,6 @@ export default function UploadInvoicePage() {
const formData = new FormData()
formData.append("file", file)
- console.log("Starting upload process...")
-
// Call our invoice extraction API
const response = await fetch("/api/extract-invoice", {
method: "POST",
@@ -71,7 +69,6 @@ export default function UploadInvoicePage() {
})
const extractedData = await response.json()
- console.log("Received response:", response.status, extractedData)
if (!response.ok) {
throw new Error(extractedData.error || "Failed to extract invoice data")
@@ -79,35 +76,29 @@ export default function UploadInvoicePage() {
// Check if we have invoice data
if (!extractedData.invoices || extractedData.invoices.length === 0) {
- console.log("No invoice data found in response")
toast({
title: "No invoice data found",
description: "Could not extract invoice information from the file.",
variant: "destructive",
})
- setIsUploading(false)
+ setIsUploading(false)
return
}
- console.log("Invoice data found, storing in sessionStorage...")
// Store the extracted data in sessionStorage to pass to the create page
sessionStorage.setItem("extractedInvoiceData", JSON.stringify(extractedData))
- console.log("Data stored in sessionStorage")
// Also store the file data for preview
if (file.type === 'application/pdf' || file.type.startsWith('image/')) {
- console.log("Processing file for preview...")
// Read the file as data URL for preview
const reader = new FileReader()
reader.onload = (e) => {
if (e.target?.result) {
- console.log("File read successfully, storing file data...")
// Store the file data URL and type
sessionStorage.setItem("invoiceFileData", e.target.result as string)
sessionStorage.setItem("invoiceFileType", file.type)
sessionStorage.setItem("invoiceFileName", file.name)
- console.log("Navigating to create page...")
// Navigate to the create page
router.push("/invoices/create")
}
@@ -115,17 +106,15 @@ export default function UploadInvoicePage() {
reader.onerror = () => {
// If file reading fails, continue without preview
console.error("Error reading file for preview")
- console.log("Navigating to create page without preview...")
router.push("/invoices/create")
}
reader.readAsDataURL(file)
} else {
- console.log("CSV file, no preview needed. Navigating to create page...")
// For CSV files, we don't need a preview
sessionStorage.removeItem("invoiceFileData")
sessionStorage.removeItem("invoiceFileType")
sessionStorage.removeItem("invoiceFileName")
- router.push("/invoices/create")
+ router.push("/invoices/create")
}
} catch (error) {
console.error("Error processing invoice:", error)
@@ -143,9 +132,9 @@ export default function UploadInvoicePage() {
}
return (
-
+
-
+
diff --git a/frontend/app/layout.tsx b/app/layout.tsx
similarity index 74%
rename from frontend/app/layout.tsx
rename to app/layout.tsx
index bf27fad..4f60001 100644
--- a/frontend/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,12 +1,9 @@
import type React from "react"
import "@/app/globals.css"
-import { Barlow } from "next/font/google"
+import { Inter } from "next/font/google"
import { ThemeProvider } from "@/components/theme-provider"
-const barlow = Barlow({
- subsets: ["latin"],
- weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
-})
+const inter = Inter({ subsets: ["latin"] })
export const metadata = {
title: "Invoice Management Dashboard",
@@ -21,7 +18,7 @@ export default function RootLayout({
}) {
return (
-
+
{children}
diff --git a/frontend/app/loading.tsx b/app/loading.tsx
similarity index 100%
rename from frontend/app/loading.tsx
rename to app/loading.tsx
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..b2e6564
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,90 @@
+import { Search, SlidersHorizontal, Bell } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import Sidebar from "@/components/sidebar"
+import InvoiceTable from "@/components/invoice-table"
+import Link from "next/link"
+
+export default function Dashboard() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Invoices
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All
+
+ Review
+ In Approval
+ Approved
+ Paid
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/app/purchase-orders/loading.tsx b/app/purchase-orders/loading.tsx
similarity index 100%
rename from frontend/app/purchase-orders/loading.tsx
rename to app/purchase-orders/loading.tsx
diff --git a/app/purchase-orders/page.tsx b/app/purchase-orders/page.tsx
new file mode 100644
index 0000000..b06794f
--- /dev/null
+++ b/app/purchase-orders/page.tsx
@@ -0,0 +1,81 @@
+import { Search, SlidersHorizontal, Bell } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import Sidebar from "@/components/sidebar"
+import PurchaseOrderTable from "@/components/purchase-order-table"
+
+export default function PurchaseOrdersPage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Purchase Orders
+
+
+
+
+
+
+
+
+
+
+
+
+ All
+
+ Pending
+ Approved
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/backend/.gitignore b/backend/.gitignore
deleted file mode 100644
index b664b1e..0000000
--- a/backend/.gitignore
+++ /dev/null
@@ -1,31 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# Environment variables
-.env
-.env.local
-.env.production
-
-# Virtual environment
-venv/
-env/
-ENV/
-
-# IDE
-.vscode/
-.idea/
-*.swp
-*.swo
-
-# OS
-.DS_Store
-Thumbs.db
-
-# Logs
-*.log
-
-# Temporary files
-temp/
-tmp/
\ No newline at end of file
diff --git a/backend/Procfile b/backend/Procfile
deleted file mode 100644
index 430bde1..0000000
--- a/backend/Procfile
+++ /dev/null
@@ -1 +0,0 @@
-web: python manage.py migrate && python manage.py collectstatic --noinput && gunicorn invoice_backend.wsgi:application --bind 0.0.0.0:$PORT
\ No newline at end of file
diff --git a/backend/README.md b/backend/README.md
deleted file mode 100644
index b9a0748..0000000
--- a/backend/README.md
+++ /dev/null
@@ -1,183 +0,0 @@
-# Invoice Processing Backend
-
-A Django REST Framework backend for invoice processing with AI-powered extraction capabilities.
-
-## Features
-
-- **Invoice Management**: Complete CRUD operations for invoices with line items
-- **Purchase Orders**: Track and manage purchase orders
-- **Goods Received**: Record goods received against purchase orders
-- **AI Invoice Extraction**: Extract invoice data from PDFs, images, and CSV files using Anthropic Claude or AWS Bedrock
-- **RESTful API**: Full REST API with filtering, searching, and pagination
-- **Database Models**: Comprehensive data models with relationships and validation
-
-## Tech Stack
-
-- **Django 5.0.1**: Web framework
-- **Django REST Framework**: API framework
-- **PostgreSQL**: Production database (SQLite for development)
-- **Anthropic Claude**: AI invoice extraction
-- **AWS Bedrock**: Alternative AI service
-- **Celery**: Background task processing
-- **Redis**: Caching and task queue
-
-## API Endpoints
-
-### Core Resources
-
-- `GET /api/companies/` - List companies
-- `GET /api/vendors/` - List vendors
-- `GET /api/items/` - List items/products
-- `GET /api/invoices/` - List invoices with line items
-- `GET /api/purchase-orders/` - List purchase orders
-- `GET /api/goods-received/` - List goods received
-
-### Invoice Extraction
-
-- `POST /api/extract-invoice/` - Upload and extract invoice data
-- `GET /api/extraction-jobs/` - List extraction jobs
-- `GET /api/extracted-invoices/` - List extracted invoice data
-
-### Health Check
-
-- `GET /api/health/` - Health check endpoint
-
-## Setup
-
-### Local Development
-
-1. **Install dependencies**:
-
- ```bash
- pip install -r requirements.txt
- ```
-
-2. **Set up environment variables** (create `.env` file):
-
- ```env
- SECRET_KEY=your-secret-key
- DEBUG=True
- ANTHROPIC_API_KEY=your-anthropic-key # Optional
- AWS_ACCESS_KEY_ID=your-aws-key # Optional
- AWS_SECRET_ACCESS_KEY=your-aws-secret # Optional
- AWS_DEFAULT_REGION=us-east-1 # Optional
- ```
-
-3. **Run migrations**:
-
- ```bash
- python manage.py migrate
- ```
-
-4. **Load sample data**:
-
- ```bash
- python manage.py load_csv_data
- ```
-
-5. **Start development server**:
- ```bash
- python manage.py runserver 8000
- ```
-
-### Production Deployment (Railway)
-
-1. **Connect your repository** to Railway
-2. **Set environment variables**:
-
- - `SECRET_KEY`: Django secret key
- - `DEBUG`: False
- - `ALLOWED_HOSTS`: your-domain.railway.app
- - `DATABASE_URL`: (automatically provided by Railway PostgreSQL)
- - `ANTHROPIC_API_KEY`: (optional)
- - `AWS_ACCESS_KEY_ID`: (optional)
- - `AWS_SECRET_ACCESS_KEY`: (optional)
-
-3. **Deploy**: Railway will automatically deploy using the `railway.json` configuration
-
-## Data Models
-
-### Core Models
-
-- **Company**: Client companies
-- **Vendor**: Supplier companies
-- **Item**: Products/services
-- **Invoice**: Invoice headers with line items
-- **PurchaseOrder**: Purchase orders with line items
-- **GoodsReceived**: Goods received records
-
-### Extraction Models
-
-- **InvoiceExtractionJob**: Track extraction jobs
-- **ExtractedInvoice**: Raw extracted invoice data
-- **ExtractedLineItem**: Extracted line item data
-
-## Invoice Extraction
-
-The system supports multiple file types:
-
-- **PDF**: Converted to images and processed with AI
-- **Images**: JPG, JPEG, PNG processed directly
-- **CSV**: Parsed directly without AI
-
-### AI Services
-
-1. **Anthropic Claude**: Primary AI service (requires API key)
-2. **AWS Bedrock**: Alternative AI service (requires AWS credentials)
-3. **Mock Service**: Returns demo data when no AI services are configured
-
-## Management Commands
-
-- `python manage.py load_csv_data`: Load sample data from CSV files
-- `python manage.py migrate`: Run database migrations
-- `python manage.py collectstatic`: Collect static files for production
-
-## API Usage Examples
-
-### Get all invoices
-
-```bash
-curl http://localhost:8000/api/invoices/
-```
-
-### Extract invoice from file
-
-```bash
-curl -X POST -F "file=@invoice.pdf" http://localhost:8000/api/extract-invoice/
-```
-
-### Filter invoices by vendor
-
-```bash
-curl "http://localhost:8000/api/invoices/?vendor=1"
-```
-
-### Search invoices
-
-```bash
-curl "http://localhost:8000/api/invoices/?search=INV-2025"
-```
-
-## Development
-
-### Adding New Features
-
-1. Create models in appropriate app
-2. Create serializers for API representation
-3. Create viewsets for API endpoints
-4. Register viewsets in main URLs
-5. Run migrations
-
-### Testing
-
-```bash
-python manage.py test
-```
-
-### Code Style
-
-Follow Django coding conventions and PEP 8 guidelines.
-
-## License
-
-MIT License
diff --git a/backend/__pycache__/anthropic_client.cpython-311.pyc b/backend/__pycache__/anthropic_client.cpython-311.pyc
new file mode 100644
index 0000000..a94d600
Binary files /dev/null and b/backend/__pycache__/anthropic_client.cpython-311.pyc differ
diff --git a/backend/__pycache__/api.cpython-311.pyc b/backend/__pycache__/api.cpython-311.pyc
new file mode 100644
index 0000000..c7fd53d
Binary files /dev/null and b/backend/__pycache__/api.cpython-311.pyc differ
diff --git a/backend/__pycache__/bedrock_client.cpython-311.pyc b/backend/__pycache__/bedrock_client.cpython-311.pyc
new file mode 100644
index 0000000..4fdea97
Binary files /dev/null and b/backend/__pycache__/bedrock_client.cpython-311.pyc differ
diff --git a/backend/__pycache__/image_processor.cpython-311.pyc b/backend/__pycache__/image_processor.cpython-311.pyc
new file mode 100644
index 0000000..46113c9
Binary files /dev/null and b/backend/__pycache__/image_processor.cpython-311.pyc differ
diff --git a/backend/__pycache__/prompts.cpython-311.pyc b/backend/__pycache__/prompts.cpython-311.pyc
new file mode 100644
index 0000000..3921e42
Binary files /dev/null and b/backend/__pycache__/prompts.cpython-311.pyc differ
diff --git a/backend/ai_engineering/__init__.py b/backend/ai_engineering/__init__.py
deleted file mode 100644
index 68cc560..0000000
--- a/backend/ai_engineering/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""
-AI Engineering Package
-
-This package contains all AI/LLM related functionality including:
-- Client implementations for various AI services (Anthropic, AWS Bedrock)
-- Invoice extraction logic
-- Image processing utilities
-- AI prompts and templates
-"""
-
-from .anthropic_client import AnthropicClient
-from .bedrock_client import BedrockClient
-from .extract import extract_invoice_from_file, extract_invoice_from_csv
-from .image_processor import get_image_from_pdf
-from .prompts import INVOICE_EXTRACTION_PROMPT
-
-__all__ = [
- 'AnthropicClient',
- 'BedrockClient',
- 'extract_invoice_from_file',
- 'extract_invoice_from_csv',
- 'get_image_from_pdf',
- 'INVOICE_EXTRACTION_PROMPT'
-]
\ No newline at end of file
diff --git a/backend/ai_engineering/anthropic_client.py b/backend/ai_engineering/anthropic_client.py
index 9b3b35c..b7849c8 100644
--- a/backend/ai_engineering/anthropic_client.py
+++ b/backend/ai_engineering/anthropic_client.py
@@ -1,13 +1,11 @@
-import base64
-import json
import anthropic
+import json
import os
import sys
import re
-from typing import Dict, Any, Optional
+from typing import Dict, Any, Union, List, Optional
from decimal import Decimal
-from datetime import datetime
-from .prompts import INVOICE_EXTRACTION_PROMPT
+from prompts import INVOICE_EXTRACTION_PROMPT
from dotenv import load_dotenv
# Load environment variables from .env file
@@ -21,15 +19,15 @@ def __init__(self):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = "claude-3-5-sonnet-20240620"
- def _parse_numeric(self, value: str) -> float:
+ def _parse_numeric(self, value: str) -> Optional[float]:
"""Parse numeric values from strings, handling various formats."""
- if not value or not isinstance(value, str):
- return 0.0
+ if not value or not isinstance(value, str) or value.strip() == "":
+ return None
try:
cleaned = "".join(c for c in value if c.isdigit() or c in ".-")
- return float(cleaned)
+ return float(cleaned) if cleaned else None
except (ValueError, TypeError):
- return 0.0
+ return None
def _parse_line_items(self, items: list) -> list:
"""Parse numeric values in line items."""
@@ -41,7 +39,7 @@ def _parse_line_items(self, items: list) -> list:
parsed_item[field] = self._parse_numeric(str(parsed_item[field]))
else:
# Handle cases where a numeric field might be missing or None
- parsed_item[field] = 0.0 # Default to 0.0 or handle as appropriate
+ parsed_item[field] = None
parsed_items.append(parsed_item)
return parsed_items
@@ -64,26 +62,33 @@ def _extract_json_from_text(self, text: str) -> str:
# If all else fails, return the original text
return text
- def extract_invoice_data(self, image_base64: str) -> Dict[str, Any]:
- """Extract invoice data from an image using Anthropic's Claude."""
+ def extract_invoice_data(self, image_base64: Union[str, List[str]]) -> Dict[str, Any]:
+ """Extract invoice data from an image or list of images using Anthropic's Claude."""
try:
+ # Always convert to list for consistent handling
+ images = [image_base64] if isinstance(image_base64, str) else image_base64
+ print(f"Processing {len(images)} image(s) with Anthropic Claude...", file=sys.stderr)
+
+ # Prepare the message content with all images
+ content = []
+ for img in images:
+ content.append({
+ "type": "image",
+ "source": {
+ "type": "base64",
+ "media_type": "image/jpeg",
+ "data": img,
+ },
+ })
+ content.append({"type": "text", "text": INVOICE_EXTRACTION_PROMPT})
+
response = self.client.messages.create(
model=self.model,
max_tokens=2048,
messages=[
{
"role": "user",
- "content": [
- {
- "type": "image",
- "source": {
- "type": "base64",
- "media_type": "image/jpeg",
- "data": image_base64,
- },
- },
- {"type": "text", "text": INVOICE_EXTRACTION_PROMPT}
- ],
+ "content": content,
}
],
)
@@ -102,15 +107,17 @@ def extract_invoice_data(self, image_base64: str) -> Dict[str, Any]:
if "invoices" in extracted_data:
for invoice in extracted_data["invoices"]:
- invoice["amount"] = self._parse_numeric(str(invoice.get("amount", "0")))
- invoice["tax_amount"] = self._parse_numeric(str(invoice.get("tax_amount", "0")))
- invoice["payment_term_days"] = self._parse_numeric(str(invoice.get("payment_term_days", "0")))
+ invoice["amount"] = self._parse_numeric(str(invoice.get("amount", "")))
+ invoice["tax_amount"] = self._parse_numeric(str(invoice.get("tax_amount", "")))
+ invoice["payment_term_days"] = self._parse_numeric(str(invoice.get("payment_term_days", "")))
if "line_items" in invoice and isinstance(invoice["line_items"], list):
invoice["line_items"] = self._parse_line_items(invoice["line_items"])
else:
invoice["line_items"] = []
+ print(f"Found {len(extracted_data['invoices'])} invoices", file=sys.stderr)
else:
extracted_data["invoices"] = []
+ print("No invoices found", file=sys.stderr)
return extracted_data
diff --git a/backend/ai_engineering/api.py b/backend/ai_engineering/api.py
new file mode 100644
index 0000000..31f17d3
--- /dev/null
+++ b/backend/ai_engineering/api.py
@@ -0,0 +1,198 @@
+import os
+import base64
+import tempfile
+from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks
+from fastapi.middleware.cors import CORSMiddleware
+import uvicorn
+from typing import Optional, Dict, Any
+import shutil
+from image_processor import get_image_from_pdf
+from anthropic_client import AnthropicClient
+from bedrock_client import BedrockClient
+import csv
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv()
+
+app = FastAPI(title="Invoice Extraction API")
+
+# Configure CORS
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["http://localhost:3000"], # Update with your frontend URL
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+def extract_invoice_from_file(file_path: str) -> Dict[str, Any]:
+ """Process a file (PDF or image) and extract invoice data."""
+ try:
+ # Determine file type based on extension
+ _, ext = os.path.splitext(file_path)
+ ext = ext.lower()
+
+ # Read the file
+ with open(file_path, 'rb') as f:
+ file_bytes = f.read()
+
+ # Process based on file type
+ if ext == '.pdf':
+ # Convert PDF to image
+ image_base64 = get_image_from_pdf(file_bytes)
+ if not image_base64:
+ return {"error": "Failed to process PDF file"}
+ elif ext in ['.jpg', '.jpeg', '.png']:
+ # For image files, encode directly to base64
+ image_base64 = base64.b64encode(file_bytes).decode('utf-8')
+ else:
+ return {"error": f"Unsupported file type: {ext}"}
+
+ # Choose which client to use based on environment variables
+ if os.getenv('ANTHROPIC_API_KEY'):
+ client = AnthropicClient()
+ elif os.environ.get('AWS_DEFAULT_REGION'): # Check if AWS credentials are available
+ client = BedrockClient()
+ else:
+ # If no API keys are available, return a mock response for testing
+ return {
+ "document_type": "invoice",
+ "invoices": [{
+ "number": "INV-DEMO-123",
+ "po_number": "PO-456",
+ "amount": 1250.00,
+ "tax_amount": 75.00,
+ "currency_code": "USD",
+ "date": "2024-09-01",
+ "due_date": "2024-09-30",
+ "payment_term_days": 30,
+ "vendor": "Demo Company Ltd",
+ "line_items": [
+ {
+ "description": "Professional Services",
+ "quantity": 10,
+ "unit_price": 125.00,
+ "total": 1250.00
+ }
+ ]
+ }]
+ }
+
+ # Extract invoice data
+ result = client.extract_invoice_data(image_base64)
+ if result:
+ return result
+ else:
+ return {"error": "Failed to extract invoice data"}
+
+ except Exception as e:
+ return {"error": f"Error processing file: {str(e)}"}
+
+def extract_invoice_from_csv(file_path: str) -> Dict[str, Any]:
+ """Parse a CSV file and extract invoice data in the expected format."""
+ try:
+ invoices = []
+ with open(file_path, 'r', newline='') as csvfile:
+ reader = csv.DictReader(csvfile)
+ for row in reader:
+ # Map CSV columns to our invoice structure
+ invoice = {
+ "number": row.get("invoice_number", ""),
+ "po_number": row.get("po_number", ""),
+ "amount": float(row.get("amount", 0)),
+ "tax_amount": float(row.get("tax_amount", 0)),
+ "currency_code": row.get("currency_code", "USD"),
+ "date": row.get("date", ""),
+ "due_date": row.get("due_date", ""),
+ "payment_term_days": int(row.get("payment_term_days", 0)),
+ "vendor": row.get("vendor", ""),
+ "line_items": []
+ }
+
+ # Add line items if they exist in the CSV
+ # This assumes your CSV has line items in some format
+ # You may need to adjust this based on your CSV structure
+ if "line_item_desc" in row and row["line_item_desc"]:
+ invoice["line_items"].append({
+ "description": row.get("line_item_desc", ""),
+ "quantity": float(row.get("line_item_qty", 1)),
+ "unit_price": float(row.get("line_item_price", 0)),
+ "total": float(row.get("line_item_total", 0))
+ })
+
+ invoices.append(invoice)
+
+ return {
+ "document_type": "invoice",
+ "invoices": invoices
+ }
+
+ except Exception as e:
+ return {"error": f"Error processing CSV file: {str(e)}"}
+
+def cleanup_temp_file(file_path: str):
+ """Remove temporary file after processing."""
+ try:
+ if os.path.exists(file_path):
+ os.unlink(file_path)
+ except Exception as e:
+ print(f"Error cleaning up temp file: {e}")
+
+@app.post("/extract-invoice")
+async def extract_invoice(
+ background_tasks: BackgroundTasks,
+ file: UploadFile = File(...)
+) -> Dict[str, Any]:
+ """
+ Extract invoice data from an uploaded file.
+
+ Accepts PDF, images (JPG, PNG), or CSV files.
+ """
+ if not file:
+ raise HTTPException(status_code=400, detail="No file uploaded")
+
+ # Get the file extension
+ file_extension = os.path.splitext(file.filename)[1].lower()
+
+ # Check if the file type is supported
+ if file_extension not in ['.pdf', '.jpg', '.jpeg', '.png', '.csv']:
+ raise HTTPException(
+ status_code=400,
+ detail="Unsupported file type. Please upload a PDF, JPG, JPEG, PNG, or CSV file."
+ )
+
+ # Create a temporary file
+ with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
+ # Write the file contents to the temporary file
+ shutil.copyfileobj(file.file, temp_file)
+ temp_file_path = temp_file.name
+
+ # Schedule cleanup of the temporary file
+ background_tasks.add_task(cleanup_temp_file, temp_file_path)
+
+ try:
+ # Process the file based on type
+ if file_extension == '.csv':
+ result = extract_invoice_from_csv(temp_file_path)
+ else:
+ result = extract_invoice_from_file(temp_file_path)
+
+ if "error" in result:
+ raise HTTPException(status_code=500, detail=result["error"])
+
+ return result
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.get("/health")
+async def health_check():
+ """Health check endpoint."""
+ return {"status": "ok"}
+
+if __name__ == "__main__":
+ # Get port from environment variable or use default
+ port = int(os.environ.get("PORT", 8000))
+
+ # Run the FastAPI server
+ uvicorn.run(app, host="0.0.0.0", port=port)
\ No newline at end of file
diff --git a/backend/ai_engineering/bedrock_client.py b/backend/ai_engineering/bedrock_client.py
index 340c5a8..9bb368e 100644
--- a/backend/ai_engineering/bedrock_client.py
+++ b/backend/ai_engineering/bedrock_client.py
@@ -1,12 +1,10 @@
-import base64
-import json
import boto3
+import json
import os
import sys
-from typing import Dict, Any, Optional
+from typing import Dict, Any, Union, List, Optional
from decimal import Decimal
-from .prompts import INVOICE_EXTRACTION_PROMPT
-from dotenv import load_dotenv
+from prompts import INVOICE_EXTRACTION_PROMPT
# Set AWS region in environment variable
os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
@@ -21,16 +19,16 @@ def __init__(self):
"anthropic.claude-3-5-sonnet-20240620-v1:0" # Using Claude 3.5 Sonnet
)
- def _parse_numeric(self, value: str) -> float:
+ def _parse_numeric(self, value: str) -> Optional[float]:
"""Parse numeric values from strings, handling various formats."""
- if not value or not isinstance(value, str):
- return 0.0
+ if not value or not isinstance(value, str) or value.strip() == "":
+ return None
try:
# Remove currency symbols and other non-numeric characters except decimal point and minus
cleaned = "".join(c for c in value if c.isdigit() or c in ".-")
- return float(cleaned)
+ return float(cleaned) if cleaned else None
except (ValueError, TypeError):
- return 0.0
+ return None
def _parse_line_items(self, items: list) -> list:
"""Parse numeric values in line items."""
@@ -43,17 +41,33 @@ def _parse_line_items(self, items: list) -> list:
parsed_items.append(parsed_item)
return parsed_items
- def extract_invoice_data(self, image_base64: str) -> Dict[str, Any]:
+ def extract_invoice_data(self, image_base64: Union[str, List[str]]) -> Dict[str, Any]:
"""
- Extract invoice data using AWS Bedrock's Claude model from an image.
+ Extract invoice data using AWS Bedrock's Claude model from an image or list of images.
Args:
- image_base64 (str): Base64 encoded image of the invoice
+ image_base64 (Union[str, List[str]]): Base64 encoded image(s) of the invoice(s)
Returns:
Dict[str, Any]: Extracted invoice data
"""
try:
+ # Always convert to list for consistent handling
+ images = [image_base64] if isinstance(image_base64, str) else image_base64
+ print(f"Processing {len(images)} image(s) with AWS Bedrock...", file=sys.stderr)
+
+ # Prepare the message content with all images
+ content = [{"type": "text", "text": INVOICE_EXTRACTION_PROMPT}]
+ for img in images:
+ content.append({
+ "type": "image",
+ "source": {
+ "type": "base64",
+ "media_type": "image/jpeg",
+ "data": img,
+ },
+ })
+
response = self.client.invoke_model(
modelId=self.model_id,
body=json.dumps(
@@ -63,17 +77,7 @@ def extract_invoice_data(self, image_base64: str) -> Dict[str, Any]:
"messages": [
{
"role": "user",
- "content": [
- {"type": "text", "text": INVOICE_EXTRACTION_PROMPT},
- {
- "type": "image",
- "source": {
- "type": "base64",
- "media_type": "image/jpeg",
- "data": image_base64,
- },
- },
- ],
+ "content": content,
}
],
}
@@ -91,13 +95,16 @@ def extract_invoice_data(self, image_base64: str) -> Dict[str, Any]:
if extracted_data.get("invoices"):
for invoice in extracted_data["invoices"]:
# Parse main invoice amounts
- invoice["amount"] = self._parse_numeric(str(invoice.get("amount", "0")))
- invoice["tax_amount"] = self._parse_numeric(str(invoice.get("tax_amount", "0")))
- invoice["payment_term_days"] = self._parse_numeric(str(invoice.get("payment_term_days", "0")))
+ invoice["amount"] = self._parse_numeric(str(invoice.get("amount", "")))
+ invoice["tax_amount"] = self._parse_numeric(str(invoice.get("tax_amount", "")))
+ invoice["payment_term_days"] = self._parse_numeric(str(invoice.get("payment_term_days", "")))
# Parse line items
if "line_items" in invoice:
invoice["line_items"] = self._parse_line_items(invoice["line_items"])
+ print(f"Found {len(extracted_data['invoices'])} invoices", file=sys.stderr)
+ else:
+ print("No invoices found", file=sys.stderr)
return extracted_data
diff --git a/backend/ai_engineering/extract.py b/backend/ai_engineering/extract.py
index 66ce7b8..5366e89 100644
--- a/backend/ai_engineering/extract.py
+++ b/backend/ai_engineering/extract.py
@@ -3,12 +3,10 @@
import json
import base64
import os
-import tempfile
+from image_processor import get_image_from_pdf
+from anthropic_client import AnthropicClient
+from bedrock_client import BedrockClient
import csv
-from .image_processor import get_image_from_pdf
-from .anthropic_client import AnthropicClient
-from .bedrock_client import BedrockClient
-from typing import Dict, Any
def extract_invoice_from_file(file_path):
"""Process a file (PDF or image) and extract invoice data."""
@@ -23,13 +21,13 @@ def extract_invoice_from_file(file_path):
# Process based on file type
if ext == '.pdf':
- # Convert PDF to image
- image_base64 = get_image_from_pdf(file_bytes)
- if not image_base64:
+ # Convert PDF to list of images
+ image_base64_list = get_image_from_pdf(file_bytes)
+ if not image_base64_list:
return {"error": "Failed to process PDF file"}
elif ext in ['.jpg', '.jpeg', '.png']:
# For image files, encode directly to base64
- image_base64 = base64.b64encode(file_bytes).decode('utf-8')
+ image_base64_list = [base64.b64encode(file_bytes).decode('utf-8')]
else:
return {"error": f"Unsupported file type: {ext}"}
@@ -63,9 +61,15 @@ def extract_invoice_from_file(file_path):
}]
}
- # Extract invoice data
- result = client.extract_invoice_data(image_base64)
- if result:
+ # For PDFs, send all pages at once. For single images, send just that image
+ if ext == '.pdf':
+ print(f"Processing PDF with {len(image_base64_list)} pages...", file=sys.stderr)
+ result = client.extract_invoice_data(image_base64_list)
+ else:
+ print("Processing single image...", file=sys.stderr)
+ result = client.extract_invoice_data(image_base64_list[0])
+
+ if result and 'invoices' in result:
return result
else:
return {"error": "Failed to extract invoice data"}
diff --git a/backend/ai_engineering/image_processor.py b/backend/ai_engineering/image_processor.py
index 71ebcad..48863fc 100644
--- a/backend/ai_engineering/image_processor.py
+++ b/backend/ai_engineering/image_processor.py
@@ -54,20 +54,23 @@ def preprocess_pdf_page_image(
elapsed_time=time() - start_time,
)
-def get_image_from_pdf(pdf_bytes: bytes) -> Optional[str]:
- """Convert PDF to base64 encoded image."""
+def get_image_from_pdf(pdf_bytes: bytes) -> Optional[list[str]]:
+ """Convert PDF to list of base64 encoded images, one per page."""
try:
+ images = []
with fitz.Document(stream=pdf_bytes, filetype="pdf") as doc:
- # Get the first page
- page = doc[0]
- # Extract image
- image_bytes = extract_image_page_bytes(page)
- # Convert to OpenCV format
- cv_image = bytes_to_cv2(image_bytes)
- # Preprocess
- processed_image = preprocess_pdf_page_image(cv_image)
- # Convert to base64
- return base64.b64encode(processed_image.data).decode("utf-8")
+ # Process each page
+ for page_num in range(len(doc)):
+ page = doc[page_num]
+ # Extract image
+ image_bytes = extract_image_page_bytes(page)
+ # Convert to OpenCV format
+ cv_image = bytes_to_cv2(image_bytes)
+ # Preprocess
+ processed_image = preprocess_pdf_page_image(cv_image)
+ # Convert to base64 and add to list
+ images.append(base64.b64encode(processed_image.data).decode("utf-8"))
+ return images if images else None
except Exception as e:
print(f"Error processing PDF: {str(e)}", file=sys.stderr)
return None
\ No newline at end of file
diff --git a/backend/ai_engineering/matching_utils.py b/backend/ai_engineering/matching_utils.py
new file mode 100644
index 0000000..da2f8d0
--- /dev/null
+++ b/backend/ai_engineering/matching_utils.py
@@ -0,0 +1,147 @@
+"""
+Matching utilities for reference comparison in 3-way matching.
+Simple prototype implementation for demo purposes.
+"""
+
+from typing import List, Dict, Tuple, Optional
+
+
+def levenshtein_distance(s1: str, s2: str) -> int:
+ """
+ Calculate the Levenshtein distance between two strings.
+ Simple implementation for prototype use.
+ """
+ if len(s1) < len(s2):
+ return levenshtein_distance(s2, s1)
+
+ if len(s2) == 0:
+ return len(s1)
+
+ previous_row = list(range(len(s2) + 1))
+ for i, c1 in enumerate(s1):
+ current_row = [i + 1]
+ for j, c2 in enumerate(s2):
+ insertions = previous_row[j + 1] + 1
+ deletions = current_row[j] + 1
+ substitutions = previous_row[j] + (c1 != c2)
+ current_row.append(min(insertions, deletions, substitutions))
+ previous_row = current_row
+
+ return previous_row[-1]
+
+
+def normalize_reference(ref: str) -> str:
+ """
+ Normalize a reference string for comparison.
+ Removes whitespace and converts to uppercase.
+ """
+ if not ref:
+ return ""
+ return ref.strip().upper()
+
+
+def get_match_type(ref1: str, ref2: str, threshold: int = 1) -> str:
+ """
+ Determine the type of match between two reference strings.
+
+ Args:
+ ref1: First reference string
+ ref2: Second reference string
+ threshold: Maximum edit distance for close match
+
+ Returns:
+ 'exact': Exact match
+ 'close': Close match (within threshold)
+ 'none': No match
+ """
+ if not ref1 or not ref2:
+ return 'none'
+
+ norm_ref1 = normalize_reference(ref1)
+ norm_ref2 = normalize_reference(ref2)
+
+ if norm_ref1 == norm_ref2:
+ return 'exact'
+
+ distance = levenshtein_distance(norm_ref1, norm_ref2)
+
+ if distance <= threshold:
+ return 'close'
+
+ return 'none'
+
+
+def is_reference_match(ref1: str, ref2: str, threshold: int = 1) -> bool:
+ """
+ Check if two references match (exact or close).
+
+ Args:
+ ref1: First reference string
+ ref2: Second reference string
+ threshold: Maximum edit distance for close match
+
+ Returns:
+ True if references match (exact or close), False otherwise
+ """
+ match_type = get_match_type(ref1, ref2, threshold)
+ return match_type in ['exact', 'close']
+
+
+def find_matching_references(
+ extracted_ref: str,
+ reference_list: List[str],
+ threshold: int = 1
+) -> Dict[str, List[str]]:
+ """
+ Find matching references in a list, categorized by match type.
+
+ Args:
+ extracted_ref: The reference extracted from document
+ reference_list: List of references to compare against
+ threshold: Maximum edit distance for close match
+
+ Returns:
+ Dictionary with 'exact' and 'close' keys containing lists of matches
+ """
+ results = {
+ 'exact': [],
+ 'close': []
+ }
+
+ for ref in reference_list:
+ match_type = get_match_type(extracted_ref, ref, threshold)
+ if match_type == 'exact':
+ results['exact'].append(ref)
+ elif match_type == 'close':
+ results['close'].append(ref)
+
+ return results
+
+
+def find_best_match(
+ extracted_ref: str,
+ reference_list: List[str],
+ threshold: int = 1
+) -> Optional[Tuple[str, str]]:
+ """
+ Find the best match for a reference in a list.
+
+ Args:
+ extracted_ref: The reference extracted from document
+ reference_list: List of references to compare against
+ threshold: Maximum edit distance for close match
+
+ Returns:
+ Tuple of (matched_reference, match_type) or None if no match
+ """
+ matches = find_matching_references(extracted_ref, reference_list, threshold)
+
+ # Prefer exact matches
+ if matches['exact']:
+ return matches['exact'][0], 'exact'
+
+ # Fall back to close matches
+ if matches['close']:
+ return matches['close'][0], 'close'
+
+ return None
\ No newline at end of file
diff --git a/backend/ai_engineering/prompts.py b/backend/ai_engineering/prompts.py
index 03dbcf6..92ae93d 100644
--- a/backend/ai_engineering/prompts.py
+++ b/backend/ai_engineering/prompts.py
@@ -1,37 +1,77 @@
-"""
-Prompt templates for AI models used in invoice extraction.
-"""
+INVOICE_EXTRACTION_PROMPT = """These images are pages from a document sent to an accounts payable inbox.
+Your job is to identify the type of document and extract details for a number of different fields
+if the document is an invoice, credit note, or a reminder document, and return those in JSON format.
+Before extracting any information, you need to classify the document to one of the following types:
+# invoice: If the any of the images contain an invoice then return 'invoice'
+# statement: If the document is a statement, then return 'statement'
+# reminder: If the document is marked as a reminder, or is a reminder letter, list of open/pending invoices,
+or aging report, then return 'reminder'
+# credit_note: If the document is a credit note then return 'credit_note'
+# purchase_order: If the document is a purchase order then return 'purchase_order'
+# remittance_advice: If the document is a remittance advice then return 'remittance_advice'
+# other: If the document is none of the above then return 'other'
-INVOICE_EXTRACTION_PROMPT = """
-You are an expert invoice data extractor. Your task is to extract structured data from an invoice image.
+If you've classified the document as 'invoice', 'reminder', or 'credit_note', proceed with extracting the
+invoice details from the document's images.
+If the document is classified as any other type, don't extract any invoice information from it.
-Extract the following information in JSON format:
-- document_type: Should be "invoice" if this is an invoice
-- invoices: An array containing the extracted invoice(s), with each invoice having these fields:
- - number: The invoice number
- - po_number: The purchase order number (if available)
- - amount: The total invoice amount excluding tax
- - tax_amount: The tax amount (if available)
- - currency_code: The 3-letter currency code (e.g., USD, EUR, GBP)
- - date: The invoice date in YYYY-MM-DD format
- - due_date: The payment due date in YYYY-MM-DD format (if available)
- - payment_term_days: The payment terms in days (if available)
- - vendor: The vendor or supplier name
- - line_items: An array of items on the invoice, each with:
- - description: Item description
- - quantity: Item quantity
- - unit_price: Price per unit
- - total: Total for this line item
+When extracting invoice details, you must extract the details as precisely as possible and extract the
+exact answer that is provided in the form, without changing the text at all.
+If no answer has been provided for a field, then return an empty string for that field.
-EXTREMELY IMPORTANT:
-1. Return ONLY valid JSON without any explanatory text before or after
-2. Do not include phrases like "Here's the extracted data" or "In JSON format"
-3. Do not use markdown backticks - return raw JSON only
-4. Return null for missing values, not empty strings
-5. For currencies, extract the 3-letter code when possible
-6. Format dates consistently as YYYY-MM-DD
-7. Convert all numeric values to numbers, not strings
+Below are the fields that you need to extract for each invoice mention and some instructions for each field:
+- `number`: Return the invoice number for the invoice.
+- `po_number`: Return the purchase order number if present.
+- `amount`: Return the total amount in numeric format for the invoice, i.e. remove all
+non-numeric characters and make sure to handle cases where it looks like a comma is actually a decimal place.
+If the document appears to be a credit note and not an invoice, then return the number as a negative number.
+- `tax_amount`: Return the total sales tax or VAT amount in numeric if it is present in the invoice,
+remove all non-numeric characters and make sure to handle cases where it looks like a comma is actually a
+decimal place.
+- `currency_code`: Return the ISO 4217 three-letter currency code for this invoice if the currency is indicated,
+if it is not explicitly shown then infer the currency code for the currency of the nation indicated by the vendor's address.
+- `date`: Return the invoice date for this invoice in ISO format (yyyy-mm-dd). Pay close attention to the country indicated
+by the vendor's address - if it is from the USA or Canada then assume that the date has been written in month-first date format
+and convert it to ISO format appropriately.
+- `due_date`: Return the invoice due date for this invoice, if it is present, in ISO format (yyyy-mm-dd).
+Pay close attention to the country indicated by the vendor's address - if it is from the USA or Canada then
+assume that the date has been written in month-first date format and convert it to ISO format appropriately.
+- `payment_term_days`: Return the number for payment term days if it is present
+- `vendor`: Return the name of the business which has sent this invoice
+- `line_items`: Return a list of line items, where each line item contains:
+ - description: The item description
+ - quantity: The quantity as a number
+ - unit_price: The unit price as a number
+ - total: The total amount for this line item as a number
-Here's the invoice image:
-{image_base64}
-"""
\ No newline at end of file
+Your response should be formatted in JSON format, with two keys in a dictionary:
+"document_type" and "invoices".
+"document_type" should contain your classification of the document, and "invoices" should be a list of
+dictionaries containing invoice details for every extracted invoice.
+Remember, if the document is not classified as 'invoice', 'reminder', or 'credit_note', "invoices" must be an empty list.
+
+Here is an example output format for a document classified as 'invoice' containing one invoice with line items:
+{
+ "document_type": "invoice",
+ "invoices": [{
+ "number": "INV01",
+ "po_number": "PO123",
+ "amount": 100.00,
+ "tax_amount": 10.00,
+ "currency_code": "GBP",
+ "date": "2024-11-09",
+ "due_date": "2024-12-09",
+ "payment_term_days": 30,
+ "vendor": "ABC LTD",
+ "line_items": [
+ {
+ "description": "Product A",
+ "quantity": 2,
+ "unit_price": 45.00,
+ "total": 90.00
+ }
+ ]
+ }]
+}
+
+Once again, make sure to return your answers in JSON format and do not return any other text in your answer."""
\ No newline at end of file
diff --git a/backend/env.example b/backend/env.example
deleted file mode 100644
index 958494e..0000000
--- a/backend/env.example
+++ /dev/null
@@ -1,27 +0,0 @@
-# Django Configuration
-DEBUG=False
-SECRET_KEY=your-secret-key-here
-ALLOWED_HOSTS=your-railway-domain.railway.app,localhost,127.0.0.1
-
-# Database (Railway will provide this automatically)
-DATABASE_URL=postgresql://user:password@host:port/database
-
-# AI Service Configuration (choose one)
-# Option 1: Anthropic API
-ANTHROPIC_API_KEY=your-anthropic-api-key-here
-
-# Option 2: AWS Bedrock (if not using Anthropic)
-# AWS_ACCESS_KEY_ID=your-aws-access-key
-# AWS_SECRET_ACCESS_KEY=your-aws-secret-key
-# AWS_DEFAULT_REGION=us-east-1
-
-# CORS Configuration for Frontend
-CORS_ALLOWED_ORIGINS=https://your-vercel-app.vercel.app,http://localhost:3000
-
-# Static Files (for production)
-STATIC_URL=/static/
-STATIC_ROOT=staticfiles
-
-# Media Files
-MEDIA_URL=/media/
-MEDIA_ROOT=media
\ No newline at end of file
diff --git a/backend/goods_received/__init__.py b/backend/goods_received/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/goods_received/admin.py b/backend/goods_received/admin.py
deleted file mode 100644
index 8c38f3f..0000000
--- a/backend/goods_received/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/backend/goods_received/apps.py b/backend/goods_received/apps.py
deleted file mode 100644
index 2c8f097..0000000
--- a/backend/goods_received/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class GoodsReceivedConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'goods_received'
diff --git a/backend/goods_received/migrations/0001_initial.py b/backend/goods_received/migrations/0001_initial.py
deleted file mode 100644
index 58c9496..0000000
--- a/backend/goods_received/migrations/0001_initial.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Generated by Django 5.0.1 on 2025-06-02 06:37
-
-import django.core.validators
-import django.db.models.deletion
-from decimal import Decimal
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('invoices', '0001_initial'),
- ('purchase_orders', '0001_initial'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='GoodsReceived',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('gr_number', models.CharField(max_length=50, unique=True)),
- ('date', models.DateField()),
- ('overall_status', models.CharField(choices=[('PARTIAL', 'Partial'), ('COMPLETE', 'Complete'), ('REJECTED', 'Rejected')], default='PARTIAL', max_length=20)),
- ('notes', models.TextField(blank=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goods_received', to='invoices.company')),
- ('purchase_order', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='goods_received', to='purchase_orders.purchaseorder')),
- ('vendor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goods_received', to='invoices.vendor')),
- ],
- options={
- 'verbose_name': 'Goods Received',
- 'verbose_name_plural': 'Goods Received',
- 'ordering': ['-date', '-gr_number'],
- },
- ),
- migrations.CreateModel(
- name='GoodsReceivedLineItem',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('quantity_ordered', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))])),
- ('quantity_received', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))])),
- ('delivery_status', models.CharField(choices=[('PARTIAL', 'Partial'), ('COMPLETE', 'Complete'), ('REJECTED', 'Rejected')], default='PARTIAL', max_length=20)),
- ('notes', models.TextField(blank=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('goods_received', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='line_items', to='goods_received.goodsreceived')),
- ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item')),
- ],
- options={
- 'unique_together': {('goods_received', 'item')},
- },
- ),
- ]
diff --git a/backend/goods_received/migrations/__init__.py b/backend/goods_received/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/goods_received/models.py b/backend/goods_received/models.py
deleted file mode 100644
index 110ae68..0000000
--- a/backend/goods_received/models.py
+++ /dev/null
@@ -1,90 +0,0 @@
-from django.db import models
-from django.core.validators import MinValueValidator
-from decimal import Decimal
-from invoices.models import Company, Vendor, Item
-from purchase_orders.models import PurchaseOrder
-
-
-class GoodsReceived(models.Model):
- """Goods Received header model."""
- STATUS_CHOICES = [
- ('PARTIAL', 'Partial'),
- ('COMPLETE', 'Complete'),
- ('REJECTED', 'Rejected'),
- ]
-
- gr_number = models.CharField(max_length=50, unique=True)
- date = models.DateField()
-
- purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name='goods_received', null=True, blank=True)
- vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='goods_received')
- company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='goods_received')
-
- overall_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PARTIAL')
- notes = models.TextField(blank=True)
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- ordering = ['-date', '-gr_number']
- verbose_name = "Goods Received"
- verbose_name_plural = "Goods Received"
-
- def __str__(self):
- return f"{self.gr_number} - {self.vendor.name}"
-
- def update_status(self):
- """Update overall status based on line item statuses."""
- line_items = self.line_items.all()
- if not line_items.exists():
- self.overall_status = 'COMPLETE'
- else:
- statuses = set(line_items.values_list('delivery_status', flat=True))
- if len(statuses) == 1 and 'COMPLETE' in statuses:
- self.overall_status = 'COMPLETE'
- elif 'REJECTED' in statuses:
- self.overall_status = 'REJECTED'
- else:
- self.overall_status = 'PARTIAL'
- self.save()
-
-
-class GoodsReceivedLineItem(models.Model):
- """Goods Received line item model."""
- STATUS_CHOICES = [
- ('PARTIAL', 'Partial'),
- ('COMPLETE', 'Complete'),
- ('REJECTED', 'Rejected'),
- ]
-
- goods_received = models.ForeignKey(GoodsReceived, on_delete=models.CASCADE, related_name='line_items')
- item = models.ForeignKey(Item, on_delete=models.CASCADE)
-
- quantity_ordered = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.01'))])
- quantity_received = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))])
- delivery_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PARTIAL')
- notes = models.TextField(blank=True)
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- unique_together = ['goods_received', 'item']
-
- def __str__(self):
- return f"{self.goods_received.gr_number} - {self.item.description}"
-
- def save(self, *args, **kwargs):
- """Update delivery status based on quantities."""
- if self.quantity_received == 0:
- self.delivery_status = 'REJECTED'
- elif self.quantity_received >= self.quantity_ordered:
- self.delivery_status = 'COMPLETE'
- else:
- self.delivery_status = 'PARTIAL'
-
- super().save(*args, **kwargs)
-
- # Update GR overall status
- self.goods_received.update_status()
diff --git a/backend/goods_received/serializers.py b/backend/goods_received/serializers.py
deleted file mode 100644
index f9f7733..0000000
--- a/backend/goods_received/serializers.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from rest_framework import serializers
-from .models import GoodsReceived, GoodsReceivedLineItem
-from invoices.serializers import CompanySerializer, VendorSerializer, ItemSerializer
-from purchase_orders.serializers import PurchaseOrderSerializer
-
-
-class GoodsReceivedLineItemSerializer(serializers.ModelSerializer):
- item_code = serializers.CharField(source='item.item_code', read_only=True)
- item_description = serializers.CharField(source='item.description', read_only=True)
-
- class Meta:
- model = GoodsReceivedLineItem
- fields = [
- 'id', 'item', 'item_code', 'item_description',
- 'quantity_ordered', 'quantity_received', 'delivery_status',
- 'notes', 'created_at', 'updated_at'
- ]
- read_only_fields = ['id', 'delivery_status', 'created_at', 'updated_at']
-
-
-class GoodsReceivedSerializer(serializers.ModelSerializer):
- vendor_name = serializers.CharField(source='vendor.name', read_only=True)
- vendor_id = serializers.CharField(source='vendor.vendor_id', read_only=True)
- company_name = serializers.CharField(source='company.name', read_only=True)
- company_id = serializers.CharField(source='company.company_id', read_only=True)
- po_number = serializers.CharField(source='purchase_order.po_number', read_only=True)
- line_items = GoodsReceivedLineItemSerializer(many=True, read_only=True)
-
- # Frontend-expected field names
- grNumber = serializers.CharField(source='gr_number', read_only=True)
- poNumber = serializers.CharField(source='purchase_order.po_number', read_only=True)
- vendor = serializers.CharField(source='vendor.name', read_only=True)
- status = serializers.CharField(source='overall_status', read_only=True)
-
- class Meta:
- model = GoodsReceived
- fields = [
- 'id', 'gr_number', 'date',
- 'purchase_order', 'po_number',
- 'vendor', 'vendor_id', 'vendor_name',
- 'company', 'company_id', 'company_name',
- 'overall_status', 'notes',
- 'line_items',
- # Frontend-expected fields
- 'grNumber', 'poNumber', 'status',
- 'created_at', 'updated_at'
- ]
- read_only_fields = ['id', 'overall_status', 'created_at', 'updated_at']
-
-
-class GoodsReceivedCreateSerializer(serializers.ModelSerializer):
- """Serializer for creating goods received with line items."""
- line_items = GoodsReceivedLineItemSerializer(many=True)
-
- class Meta:
- model = GoodsReceived
- fields = [
- 'gr_number', 'date', 'purchase_order',
- 'vendor', 'company', 'notes',
- 'line_items'
- ]
-
- def create(self, validated_data):
- line_items_data = validated_data.pop('line_items')
- goods_received = GoodsReceived.objects.create(**validated_data)
-
- for line_item_data in line_items_data:
- GoodsReceivedLineItem.objects.create(goods_received=goods_received, **line_item_data)
-
- return goods_received
\ No newline at end of file
diff --git a/backend/goods_received/tests.py b/backend/goods_received/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/backend/goods_received/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/backend/goods_received/views.py b/backend/goods_received/views.py
deleted file mode 100644
index d682505..0000000
--- a/backend/goods_received/views.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from django.shortcuts import render
-from rest_framework import viewsets, filters
-from rest_framework.response import Response
-from rest_framework.decorators import action
-from django_filters.rest_framework import DjangoFilterBackend
-from .models import GoodsReceived, GoodsReceivedLineItem
-from .serializers import (
- GoodsReceivedSerializer, GoodsReceivedCreateSerializer, GoodsReceivedLineItemSerializer
-)
-
-
-class GoodsReceivedViewSet(viewsets.ModelViewSet):
- """ViewSet for GoodsReceived model."""
- queryset = GoodsReceived.objects.select_related('vendor', 'company', 'purchase_order').prefetch_related('line_items__item')
- serializer_class = GoodsReceivedSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['vendor', 'company', 'purchase_order', 'overall_status']
- search_fields = ['gr_number', 'vendor__name', 'company__name', 'purchase_order__po_number']
- ordering_fields = ['date', 'gr_number', 'created_at']
- ordering = ['-date', '-gr_number']
-
- def get_serializer_class(self):
- """Return appropriate serializer based on action."""
- if self.action == 'create':
- return GoodsReceivedCreateSerializer
- return GoodsReceivedSerializer
-
- @action(detail=False, methods=['get'])
- def summary(self, request):
- """Get goods received summary statistics."""
- queryset = self.filter_queryset(self.get_queryset())
-
- total_grs = queryset.count()
-
- # Group by status
- status_summary = {}
- for gr in queryset:
- status = gr.overall_status
- if status not in status_summary:
- status_summary[status] = {'count': 0}
- status_summary[status]['count'] += 1
-
- return Response({
- 'total_goods_received': total_grs,
- 'status_summary': status_summary
- })
-
-
-class GoodsReceivedLineItemViewSet(viewsets.ModelViewSet):
- """ViewSet for GoodsReceivedLineItem model."""
- queryset = GoodsReceivedLineItem.objects.select_related('goods_received', 'item')
- serializer_class = GoodsReceivedLineItemSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['goods_received', 'item', 'delivery_status']
- search_fields = ['item__item_code', 'item__description']
- ordering_fields = ['quantity_ordered', 'quantity_received', 'created_at']
- ordering = ['created_at']
diff --git a/backend/invoice_backend/__init__.py b/backend/invoice_backend/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/invoice_backend/asgi.py b/backend/invoice_backend/asgi.py
deleted file mode 100644
index 93fcdda..0000000
--- a/backend/invoice_backend/asgi.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-ASGI config for invoice_backend project.
-
-It exposes the ASGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
-"""
-
-import os
-
-from django.core.asgi import get_asgi_application
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'invoice_backend.settings')
-
-application = get_asgi_application()
diff --git a/backend/invoice_backend/settings.py b/backend/invoice_backend/settings.py
deleted file mode 100644
index 3a1495d..0000000
--- a/backend/invoice_backend/settings.py
+++ /dev/null
@@ -1,228 +0,0 @@
-"""
-Django settings for invoice_backend project.
-
-Generated by 'django-admin startproject' using Django 5.1.5.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/5.1/topics/settings/
-
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/5.1/ref/settings/
-"""
-
-import os
-import environ
-from pathlib import Path
-import logging
-
-# Initialize environment variables
-env = environ.Env(
- DEBUG=(bool, True)
-)
-
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
-BASE_DIR = Path(__file__).resolve().parent.parent
-
-# Read environment variables from .env file
-environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = env('SECRET_KEY', default='django-insecure-change-me-in-production')
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = env('DEBUG', default=True)
-
-ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[
- 'localhost',
- '127.0.0.1',
- '0.0.0.0',
- 'invoice-processing-poc-production.up.railway.app',
- '*.railway.app', # Allow any Railway subdomain
- '*.up.railway.app' # Allow any Railway production subdomain
-])
-
-# Application definition
-
-INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
-
- # Third party apps
- 'rest_framework',
- 'corsheaders',
- 'django_filters',
-
- # Local apps
- 'invoices',
- 'purchase_orders',
- 'goods_received',
- 'invoice_extraction',
-]
-
-MIDDLEWARE = [
- 'corsheaders.middleware.CorsMiddleware',
- 'django.middleware.security.SecurityMiddleware',
- 'whitenoise.middleware.WhiteNoiseMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-ROOT_URLCONF = 'invoice_backend.urls'
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ],
- },
- },
-]
-
-WSGI_APPLICATION = 'invoice_backend.wsgi.application'
-
-# Database
-# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
-
-# Default to SQLite for development, PostgreSQL for production
-database_url = env('DATABASE_URL', default=None)
-if database_url and 'postgres' in database_url:
- DATABASES = {
- 'default': env.db()
- }
-else:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'db.sqlite3',
- }
- }
-
-# Password validation
-# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
- {
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
- },
-]
-
-# Internationalization
-# https://docs.djangoproject.com/en/5.1/topics/i18n/
-
-LANGUAGE_CODE = 'en-us'
-
-TIME_ZONE = 'UTC'
-
-USE_I18N = True
-
-USE_TZ = True
-
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/5.1/howto/static-files/
-
-STATIC_URL = '/static/'
-STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
-
-# Media files
-MEDIA_URL = '/media/'
-MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
-
-# Default primary key field type
-# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
-
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
-
-# Django REST Framework configuration
-REST_FRAMEWORK = {
- 'DEFAULT_PERMISSION_CLASSES': [
- 'rest_framework.permissions.AllowAny',
- ],
- 'DEFAULT_RENDERER_CLASSES': [
- 'rest_framework.renderers.JSONRenderer',
- ],
- 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
- 'PAGE_SIZE': 20,
-}
-
-# CORS settings
-CORS_ALLOWED_ORIGINS = env.list('CORS_ALLOWED_ORIGINS', default=[
- "http://localhost:3000",
- "http://127.0.0.1:3000",
- "https://invoice-processing-frontend-xi.vercel.app",
- "https://invoice-processing-frontend-kdgqmroth-xelix-projects.vercel.app",
- "https://*.vercel.app", # Allow any Vercel deployment
-])
-
-CORS_ALLOW_CREDENTIALS = True
-
-# For development, allow all origins
-if DEBUG:
- CORS_ALLOW_ALL_ORIGINS = True
-else:
- # For production, also allow all origins temporarily for debugging
- CORS_ALLOW_ALL_ORIGINS = True
-
-# Celery configuration (for background tasks)
-CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='redis://localhost:6379/0')
-CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND', default='redis://localhost:6379/0')
-CELERY_ACCEPT_CONTENT = ['json']
-CELERY_TASK_SERIALIZER = 'json'
-CELERY_RESULT_SERIALIZER = 'json'
-CELERY_TIMEZONE = TIME_ZONE
-
-# Logging
-LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'handlers': {
- 'file': {
- 'level': 'INFO',
- 'class': 'logging.FileHandler',
- 'filename': os.path.join(BASE_DIR, 'django.log'),
- },
- 'console': {
- 'level': 'INFO',
- 'class': 'logging.StreamHandler',
- },
- },
- 'loggers': {
- 'django': {
- 'handlers': ['file', 'console'],
- 'level': 'INFO',
- 'propagate': True,
- },
- },
-}
-
-# Environment-specific settings for AI services
-ANTHROPIC_API_KEY = env('ANTHROPIC_API_KEY', default='')
-AWS_DEFAULT_REGION = env('AWS_DEFAULT_REGION', default='us-east-1')
-AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default='')
-AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', default='')
diff --git a/backend/invoice_backend/urls.py b/backend/invoice_backend/urls.py
deleted file mode 100644
index 24e100e..0000000
--- a/backend/invoice_backend/urls.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""
-URL configuration for invoice_backend project.
-
-The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/5.1/topics/http/urls/
-Examples:
-Function views
- 1. Add an import: from my_app import views
- 2. Add a URL to urlpatterns: path('', views.home, name='home')
-Class-based views
- 1. Add an import: from other_app.views import Home
- 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
-Including another URLconf
- 1. Import the include() function: from django.urls import include, path
- 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
-"""
-from django.contrib import admin
-from django.urls import path, include
-from django.conf import settings
-from django.conf.urls.static import static
-from django.http import HttpResponse, JsonResponse
-from rest_framework.routers import DefaultRouter
-from invoices.views import CompanyViewSet, VendorViewSet, ItemViewSet, InvoiceViewSet, InvoiceLineItemViewSet
-from purchase_orders.views import PurchaseOrderViewSet, PurchaseOrderLineItemViewSet
-from goods_received.views import GoodsReceivedViewSet, GoodsReceivedLineItemViewSet
-from invoice_extraction.views import InvoiceExtractionJobViewSet, ExtractedInvoiceViewSet
-from django.views.decorators.csrf import csrf_exempt
-
-# Create a router and register our viewsets with it
-router = DefaultRouter()
-router.register(r'companies', CompanyViewSet)
-router.register(r'vendors', VendorViewSet)
-router.register(r'items', ItemViewSet)
-router.register(r'invoices', InvoiceViewSet)
-router.register(r'invoice-line-items', InvoiceLineItemViewSet)
-router.register(r'purchase-orders', PurchaseOrderViewSet)
-router.register(r'purchase-order-line-items', PurchaseOrderLineItemViewSet)
-router.register(r'goods-received', GoodsReceivedViewSet)
-router.register(r'goods-received-line-items', GoodsReceivedLineItemViewSet)
-router.register(r'extraction-jobs', InvoiceExtractionJobViewSet)
-router.register(r'extracted-invoices', ExtractedInvoiceViewSet)
-
-@csrf_exempt
-def health_check(request):
- return JsonResponse({"status": "healthy"})
-
-urlpatterns = [
- path('admin/', admin.site.urls),
- path('api/', include(router.urls)),
- path('api/extract-invoice/', InvoiceExtractionJobViewSet.as_view({'post': 'upload'}), name='extract-invoice'),
- path('api/health/', health_check, name='health_check'),
-]
-
-# Serve media files during development
-if settings.DEBUG:
- urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
diff --git a/backend/invoice_backend/wsgi.py b/backend/invoice_backend/wsgi.py
deleted file mode 100644
index daec691..0000000
--- a/backend/invoice_backend/wsgi.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-WSGI config for invoice_backend project.
-
-It exposes the WSGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
-"""
-
-import os
-
-from django.core.wsgi import get_wsgi_application
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'invoice_backend.settings')
-
-application = get_wsgi_application()
diff --git a/backend/invoice_extraction/__init__.py b/backend/invoice_extraction/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/invoice_extraction/admin.py b/backend/invoice_extraction/admin.py
deleted file mode 100644
index 8c38f3f..0000000
--- a/backend/invoice_extraction/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/backend/invoice_extraction/apps.py b/backend/invoice_extraction/apps.py
deleted file mode 100644
index d6e9dad..0000000
--- a/backend/invoice_extraction/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class InvoiceExtractionConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'invoice_extraction'
diff --git a/backend/invoice_extraction/migrations/0001_initial.py b/backend/invoice_extraction/migrations/0001_initial.py
deleted file mode 100644
index c35044e..0000000
--- a/backend/invoice_extraction/migrations/0001_initial.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Generated by Django 5.0.1 on 2025-06-02 06:37
-
-import django.db.models.deletion
-import uuid
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='ExtractedInvoice',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('document_type', models.CharField(default='invoice', max_length=50)),
- ('invoice_number', models.CharField(blank=True, max_length=100)),
- ('po_number', models.CharField(blank=True, max_length=100)),
- ('amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
- ('tax_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
- ('currency_code', models.CharField(default='USD', max_length=3)),
- ('date', models.CharField(blank=True, max_length=50)),
- ('due_date', models.CharField(blank=True, max_length=50)),
- ('payment_term_days', models.IntegerField(blank=True, null=True)),
- ('vendor', models.CharField(blank=True, max_length=255)),
- ('processed_to_invoice', models.BooleanField(default=False)),
- ('processed_invoice_id', models.IntegerField(blank=True, null=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ],
- ),
- migrations.CreateModel(
- name='InvoiceExtractionJob',
- fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('original_filename', models.CharField(max_length=255)),
- ('file_type', models.CharField(max_length=10)),
- ('uploaded_file', models.FileField(upload_to='invoice_uploads/')),
- ('status', models.CharField(choices=[('PENDING', 'Pending'), ('PROCESSING', 'Processing'), ('COMPLETED', 'Completed'), ('FAILED', 'Failed')], default='PENDING', max_length=20)),
- ('error_message', models.TextField(blank=True)),
- ('ai_service_used', models.CharField(blank=True, max_length=50)),
- ('processing_time_seconds', models.FloatField(blank=True, null=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('processed_at', models.DateTimeField(blank=True, null=True)),
- ],
- options={
- 'ordering': ['-created_at'],
- },
- ),
- migrations.CreateModel(
- name='ExtractedLineItem',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('description', models.CharField(max_length=500)),
- ('quantity', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
- ('unit_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
- ('total', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('extracted_invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='line_items', to='invoice_extraction.extractedinvoice')),
- ],
- ),
- migrations.AddField(
- model_name='extractedinvoice',
- name='extraction_job',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_invoices', to='invoice_extraction.invoiceextractionjob'),
- ),
- ]
diff --git a/backend/invoice_extraction/migrations/0002_alter_extractedinvoice_currency_code_and_more.py b/backend/invoice_extraction/migrations/0002_alter_extractedinvoice_currency_code_and_more.py
deleted file mode 100644
index 2cdd4af..0000000
--- a/backend/invoice_extraction/migrations/0002_alter_extractedinvoice_currency_code_and_more.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Generated by Django 5.0.1 on 2025-06-02 08:10
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('invoice_extraction', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='extractedinvoice',
- name='currency_code',
- field=models.CharField(blank=True, default='USD', max_length=3, null=True),
- ),
- migrations.AlterField(
- model_name='extractedinvoice',
- name='date',
- field=models.CharField(blank=True, max_length=50, null=True),
- ),
- migrations.AlterField(
- model_name='extractedinvoice',
- name='due_date',
- field=models.CharField(blank=True, max_length=50, null=True),
- ),
- migrations.AlterField(
- model_name='extractedinvoice',
- name='invoice_number',
- field=models.CharField(blank=True, max_length=100, null=True),
- ),
- migrations.AlterField(
- model_name='extractedinvoice',
- name='po_number',
- field=models.CharField(blank=True, max_length=100, null=True),
- ),
- migrations.AlterField(
- model_name='extractedinvoice',
- name='vendor',
- field=models.CharField(blank=True, max_length=255, null=True),
- ),
- migrations.AlterField(
- model_name='extractedlineitem',
- name='description',
- field=models.CharField(blank=True, max_length=500, null=True),
- ),
- ]
diff --git a/backend/invoice_extraction/migrations/__init__.py b/backend/invoice_extraction/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/invoice_extraction/models.py b/backend/invoice_extraction/models.py
deleted file mode 100644
index 4ee16fd..0000000
--- a/backend/invoice_extraction/models.py
+++ /dev/null
@@ -1,78 +0,0 @@
-from django.db import models
-from django.contrib.auth.models import User
-from decimal import Decimal
-import uuid
-
-
-class InvoiceExtractionJob(models.Model):
- """Model to track invoice extraction jobs."""
- STATUS_CHOICES = [
- ('PENDING', 'Pending'),
- ('PROCESSING', 'Processing'),
- ('COMPLETED', 'Completed'),
- ('FAILED', 'Failed'),
- ]
-
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- original_filename = models.CharField(max_length=255)
- file_type = models.CharField(max_length=10) # pdf, jpg, png, csv
- uploaded_file = models.FileField(upload_to='invoice_uploads/')
-
- status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
- error_message = models.TextField(blank=True)
-
- # Processing details
- ai_service_used = models.CharField(max_length=50, blank=True) # anthropic, bedrock, mock
- processing_time_seconds = models.FloatField(null=True, blank=True)
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
- processed_at = models.DateTimeField(null=True, blank=True)
-
- class Meta:
- ordering = ['-created_at']
-
- def __str__(self):
- return f"Extraction Job {self.id} - {self.original_filename}"
-
-
-class ExtractedInvoice(models.Model):
- """Model to store extracted invoice data before it's processed into the main Invoice model."""
- extraction_job = models.ForeignKey(InvoiceExtractionJob, on_delete=models.CASCADE, related_name='extracted_invoices')
-
- # Raw extracted data
- document_type = models.CharField(max_length=50, default='invoice')
- invoice_number = models.CharField(max_length=100, null=True, blank=True)
- po_number = models.CharField(max_length=100, null=True, blank=True)
- amount = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
- tax_amount = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
- currency_code = models.CharField(max_length=3, default='USD', null=True, blank=True)
- date = models.CharField(max_length=50, null=True, blank=True) # Store as string initially for parsing
- due_date = models.CharField(max_length=50, null=True, blank=True)
- payment_term_days = models.IntegerField(null=True, blank=True)
- vendor = models.CharField(max_length=255, null=True, blank=True)
-
- # Processing status
- processed_to_invoice = models.BooleanField(default=False)
- processed_invoice_id = models.IntegerField(null=True, blank=True) # Reference to main Invoice model
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- def __str__(self):
- return f"Extracted Invoice {self.invoice_number or 'No Number'} from {self.extraction_job.original_filename}"
-
-
-class ExtractedLineItem(models.Model):
- """Model to store extracted line item data."""
- extracted_invoice = models.ForeignKey(ExtractedInvoice, on_delete=models.CASCADE, related_name='line_items')
-
- description = models.CharField(max_length=500, null=True, blank=True)
- quantity = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
- unit_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
- total = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
-
- created_at = models.DateTimeField(auto_now_add=True)
-
- def __str__(self):
- return f"Line Item: {self.description or 'No Description'} ({self.extracted_invoice.invoice_number or 'No Invoice Number'})"
diff --git a/backend/invoice_extraction/serializers.py b/backend/invoice_extraction/serializers.py
deleted file mode 100644
index d3f100e..0000000
--- a/backend/invoice_extraction/serializers.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from rest_framework import serializers
-from .models import InvoiceExtractionJob, ExtractedInvoice, ExtractedLineItem
-
-
-class ExtractedLineItemSerializer(serializers.ModelSerializer):
- class Meta:
- model = ExtractedLineItem
- fields = [
- 'id', 'description', 'quantity', 'unit_price', 'total',
- 'created_at'
- ]
- read_only_fields = ['id', 'created_at']
-
-
-class ExtractedInvoiceSerializer(serializers.ModelSerializer):
- line_items = ExtractedLineItemSerializer(many=True, read_only=True)
- number = serializers.CharField(source='invoice_number', read_only=True)
-
- class Meta:
- model = ExtractedInvoice
- fields = [
- 'id', 'document_type', 'invoice_number', 'number', 'po_number',
- 'amount', 'tax_amount', 'currency_code', 'date', 'due_date',
- 'payment_term_days', 'vendor', 'processed_to_invoice',
- 'processed_invoice_id', 'line_items',
- 'created_at', 'updated_at'
- ]
- read_only_fields = ['id', 'created_at', 'updated_at']
-
-
-class InvoiceExtractionJobSerializer(serializers.ModelSerializer):
- extracted_invoices = ExtractedInvoiceSerializer(many=True, read_only=True)
-
- class Meta:
- model = InvoiceExtractionJob
- fields = [
- 'id', 'original_filename', 'file_type', 'uploaded_file',
- 'status', 'error_message', 'ai_service_used',
- 'processing_time_seconds', 'extracted_invoices',
- 'created_at', 'updated_at', 'processed_at'
- ]
- read_only_fields = [
- 'id', 'status', 'error_message', 'ai_service_used',
- 'processing_time_seconds', 'created_at', 'updated_at', 'processed_at'
- ]
-
-
-class InvoiceExtractionUploadSerializer(serializers.Serializer):
- """Serializer for file upload."""
- file = serializers.FileField()
-
- def validate_file(self, value):
- """Validate file type and size."""
- allowed_extensions = ['.pdf', '.jpg', '.jpeg', '.png', '.csv']
- filename = value.name.lower()
-
- if not any(filename.endswith(ext) for ext in allowed_extensions):
- raise serializers.ValidationError(
- "Unsupported file type. Please upload a PDF, JPG, JPEG, PNG, or CSV file."
- )
-
- # Check file size (10MB limit)
- if value.size > 10 * 1024 * 1024:
- raise serializers.ValidationError(
- "File size too large. Please upload a file smaller than 10MB."
- )
-
- return value
\ No newline at end of file
diff --git a/backend/invoice_extraction/services.py b/backend/invoice_extraction/services.py
deleted file mode 100644
index be1c2c4..0000000
--- a/backend/invoice_extraction/services.py
+++ /dev/null
@@ -1,151 +0,0 @@
-import os
-import base64
-import tempfile
-import shutil
-from typing import Dict, Any, Optional
-from django.core.files.uploadedfile import UploadedFile
-from django.conf import settings
-from decimal import Decimal
-import json
-from datetime import datetime, timedelta
-import time
-import csv
-
-from ai_engineering.anthropic_client import AnthropicClient
-from ai_engineering.bedrock_client import BedrockClient
-from ai_engineering.image_processor import get_image_from_pdf
-
-from .models import InvoiceExtractionJob, ExtractedInvoice, ExtractedLineItem
-
-
-class InvoiceExtractionService:
- """Service for processing invoice files and extracting data."""
-
- def __init__(self):
- # Set up references to the AI classes
- self.AnthropicClient = AnthropicClient
- self.BedrockClient = BedrockClient
- self.get_image_from_pdf = get_image_from_pdf
-
- def process_file(self, extraction_job) -> Dict[str, Any]:
- """Process a file and extract invoice data."""
- try:
- # Get the file path
- file_path = extraction_job.uploaded_file.path
- file_extension = f".{extraction_job.file_type}"
-
- # Process based on file type
- if file_extension == '.csv':
- result = self._extract_invoice_from_csv(file_path)
- result['ai_service_used'] = 'csv_parser'
- else:
- result = self._extract_invoice_from_file(file_path, file_extension)
-
- return result
-
- except Exception as e:
- return {"error": f"Error processing file: {str(e)}"}
-
- def _extract_invoice_from_file(self, file_path: str, file_extension: str) -> Dict[str, Any]:
- """Process a file (PDF or image) and extract invoice data."""
- try:
- # Read the file
- with open(file_path, 'rb') as f:
- file_bytes = f.read()
-
- # Process based on file type
- if file_extension == '.pdf':
- # Convert PDF to image
- image_base64 = self.get_image_from_pdf(file_bytes)
- if not image_base64:
- return {"error": "Failed to process PDF file"}
- elif file_extension in ['.jpg', '.jpeg', '.png']:
- # For image files, encode directly to base64
- image_base64 = base64.b64encode(file_bytes).decode('utf-8')
- else:
- return {"error": f"Unsupported file type: {file_extension}"}
-
- # Choose which client to use based on environment variables
- ai_service_used = 'mock'
- if settings.ANTHROPIC_API_KEY:
- client = self.AnthropicClient()
- ai_service_used = 'anthropic'
- elif settings.AWS_DEFAULT_REGION and settings.AWS_ACCESS_KEY_ID:
- client = self.BedrockClient()
- ai_service_used = 'bedrock'
- else:
- # If no API keys are available, return a mock response for testing
- return {
- "document_type": "invoice",
- "ai_service_used": "mock",
- "invoices": [{
- "number": "INV-DEMO-123",
- "po_number": "PO-456",
- "amount": 1250.00,
- "tax_amount": 75.00,
- "currency_code": "USD",
- "date": "2024-09-01",
- "due_date": "2024-09-30",
- "payment_term_days": 30,
- "vendor": "Demo Company Ltd",
- "line_items": [
- {
- "description": "Professional Services",
- "quantity": 10,
- "unit_price": 125.00,
- "total": 1250.00
- }
- ]
- }]
- }
-
- # Extract invoice data
- result = client.extract_invoice_data(image_base64)
- if result:
- result['ai_service_used'] = ai_service_used
- return result
- else:
- return {"error": "Failed to extract invoice data"}
-
- except Exception as e:
- return {"error": f"Error processing file: {str(e)}"}
-
- def _extract_invoice_from_csv(self, file_path: str) -> Dict[str, Any]:
- """Parse a CSV file and extract invoice data in the expected format."""
- try:
- invoices = []
- with open(file_path, 'r', newline='') as csvfile:
- reader = csv.DictReader(csvfile)
- for row in reader:
- # Map CSV columns to our invoice structure
- invoice = {
- "number": row.get("invoice_number", ""),
- "po_number": row.get("po_number", ""),
- "amount": float(row.get("amount", 0)),
- "tax_amount": float(row.get("tax_amount", 0)),
- "currency_code": row.get("currency_code", "USD"),
- "date": row.get("date", ""),
- "due_date": row.get("due_date", ""),
- "payment_term_days": int(row.get("payment_term_days", 0)),
- "vendor": row.get("vendor", ""),
- "line_items": []
- }
-
- # Add line items if they exist in the CSV
- if "line_item_desc" in row and row["line_item_desc"]:
- invoice["line_items"].append({
- "description": row.get("line_item_desc", ""),
- "quantity": float(row.get("line_item_qty", 1)),
- "unit_price": float(row.get("line_item_price", 0)),
- "total": float(row.get("line_item_total", 0))
- })
-
- invoices.append(invoice)
-
- return {
- "document_type": "invoice",
- "invoices": invoices
- }
-
- except Exception as e:
- return {"error": f"Error processing CSV file: {str(e)}"}
\ No newline at end of file
diff --git a/backend/invoice_extraction/tests.py b/backend/invoice_extraction/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/backend/invoice_extraction/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/backend/invoice_extraction/views.py b/backend/invoice_extraction/views.py
deleted file mode 100644
index 50fdfd2..0000000
--- a/backend/invoice_extraction/views.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from django.shortcuts import render
-import os
-import time
-from rest_framework import viewsets, status
-from rest_framework.decorators import action
-from rest_framework.response import Response
-from rest_framework.parsers import MultiPartParser, FormParser
-from django.utils import timezone
-from .models import InvoiceExtractionJob, ExtractedInvoice, ExtractedLineItem
-from .serializers import (
- InvoiceExtractionJobSerializer, InvoiceExtractionUploadSerializer,
- ExtractedInvoiceSerializer
-)
-from .services import InvoiceExtractionService
-
-
-class InvoiceExtractionJobViewSet(viewsets.ModelViewSet):
- """ViewSet for InvoiceExtractionJob model."""
- queryset = InvoiceExtractionJob.objects.all()
- serializer_class = InvoiceExtractionJobSerializer
-
- @action(detail=False, methods=['post'], parser_classes=[MultiPartParser, FormParser])
- def upload(self, request):
- """Upload and process an invoice file."""
- serializer = InvoiceExtractionUploadSerializer(data=request.data)
- if serializer.is_valid():
- uploaded_file = serializer.validated_data['file']
-
- # Create extraction job
- job = InvoiceExtractionJob.objects.create(
- original_filename=uploaded_file.name,
- file_type=os.path.splitext(uploaded_file.name)[1].lower().replace('.', ''),
- uploaded_file=uploaded_file,
- status='PROCESSING'
- )
-
- try:
- # Process the file
- start_time = time.time()
- extraction_service = InvoiceExtractionService()
- result = extraction_service.process_file(job)
- processing_time = time.time() - start_time
-
- if result.get('error'):
- job.status = 'FAILED'
- job.error_message = result['error']
- else:
- job.status = 'COMPLETED'
- job.ai_service_used = result.get('ai_service_used', 'unknown')
-
- # Save extracted data
- for invoice_data in result.get('invoices', []):
- extracted_invoice = ExtractedInvoice.objects.create(
- extraction_job=job,
- document_type=result.get('document_type', 'invoice'),
- invoice_number=invoice_data.get('number', ''),
- po_number=invoice_data.get('po_number', ''),
- amount=invoice_data.get('amount'),
- tax_amount=invoice_data.get('tax_amount'),
- currency_code=invoice_data.get('currency_code', 'USD'),
- date=invoice_data.get('date', ''),
- due_date=invoice_data.get('due_date', ''),
- payment_term_days=invoice_data.get('payment_term_days'),
- vendor=invoice_data.get('vendor', '')
- )
-
- # Save line items
- for line_item_data in invoice_data.get('line_items', []):
- ExtractedLineItem.objects.create(
- extracted_invoice=extracted_invoice,
- description=line_item_data.get('description', ''),
- quantity=line_item_data.get('quantity'),
- unit_price=line_item_data.get('unit_price'),
- total=line_item_data.get('total')
- )
-
- job.processing_time_seconds = processing_time
- job.processed_at = timezone.now()
- job.save()
-
- # Return the job with extracted data
- response_serializer = InvoiceExtractionJobSerializer(job)
- return Response(response_serializer.data, status=status.HTTP_201_CREATED)
-
- except Exception as e:
- job.status = 'FAILED'
- job.error_message = str(e)
- job.save()
- return Response(
- {'error': f'Processing failed: {str(e)}'},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
-class ExtractedInvoiceViewSet(viewsets.ReadOnlyModelViewSet):
- """ViewSet for ExtractedInvoice model (read-only)."""
- queryset = ExtractedInvoice.objects.all()
- serializer_class = ExtractedInvoiceSerializer
diff --git a/backend/invoices/__init__.py b/backend/invoices/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/invoices/admin.py b/backend/invoices/admin.py
deleted file mode 100644
index 8c38f3f..0000000
--- a/backend/invoices/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/backend/invoices/apps.py b/backend/invoices/apps.py
deleted file mode 100644
index b68fb44..0000000
--- a/backend/invoices/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class InvoicesConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'invoices'
diff --git a/backend/invoices/management/__init__.py b/backend/invoices/management/__init__.py
deleted file mode 100644
index ccd9b3b..0000000
--- a/backend/invoices/management/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Management package
\ No newline at end of file
diff --git a/backend/invoices/management/commands/__init__.py b/backend/invoices/management/commands/__init__.py
deleted file mode 100644
index 35640dd..0000000
--- a/backend/invoices/management/commands/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Management commands package
\ No newline at end of file
diff --git a/backend/invoices/management/commands/load_csv_data.py b/backend/invoices/management/commands/load_csv_data.py
deleted file mode 100644
index 60836fc..0000000
--- a/backend/invoices/management/commands/load_csv_data.py
+++ /dev/null
@@ -1,310 +0,0 @@
-import csv
-import os
-from decimal import Decimal
-from datetime import datetime
-from django.core.management.base import BaseCommand
-from django.conf import settings
-from invoices.models import Company, Vendor, Item, Invoice, InvoiceLineItem
-from purchase_orders.models import PurchaseOrder, PurchaseOrderLineItem
-from goods_received.models import GoodsReceived, GoodsReceivedLineItem
-
-
-class Command(BaseCommand):
- help = 'Load data from CSV files into the database'
-
- def add_arguments(self, parser):
- parser.add_argument(
- '--fixtures-dir',
- type=str,
- default='fixtures',
- help='Directory containing CSV files (default: fixtures)'
- )
-
- def handle(self, *args, **options):
- fixtures_dir = options['fixtures_dir']
- base_dir = settings.BASE_DIR
- fixtures_path = os.path.join(base_dir, fixtures_dir)
-
- if not os.path.exists(fixtures_path):
- self.stdout.write(
- self.style.ERROR(f'Fixtures directory not found: {fixtures_path}')
- )
- return
-
- self.stdout.write(
- self.style.SUCCESS(f'Starting CSV data loading from: {fixtures_path}')
- )
-
- # Load data in order (dependencies first)
- try:
- self.load_companies_and_vendors(fixtures_path)
- self.load_items(fixtures_path)
- self.load_purchase_orders(fixtures_path)
- self.load_goods_received(fixtures_path)
- self.load_invoices(fixtures_path)
-
- self.stdout.write(
- self.style.SUCCESS('Successfully loaded all CSV data!')
- )
- except Exception as e:
- self.stdout.write(
- self.style.ERROR(f'Error loading CSV data: {e}')
- )
- raise
-
- def load_companies_and_vendors(self, fixtures_path):
- """Load companies and vendors from CSV files."""
- self.stdout.write('Loading companies and vendors...')
-
- companies = set()
- vendors = set()
-
- # Extract companies and vendors from all CSV files
- for csv_file in ['invoices.csv', 'purchase-orders.csv', 'goods-received.csv']:
- csv_path = os.path.join(fixtures_path, csv_file)
- if os.path.exists(csv_path):
- with open(csv_path, 'r') as file:
- reader = csv.DictReader(file)
- for row in reader:
- if 'Company_ID' in row and 'Company_Name' in row:
- companies.add((row['Company_ID'], row['Company_Name']))
- if 'Vendor_ID' in row and 'Vendor_Name' in row:
- vendors.add((row['Vendor_ID'], row['Vendor_Name']))
-
- # Create companies
- companies_created = 0
- for company_id, company_name in companies:
- company, created = Company.objects.get_or_create(
- company_id=company_id,
- defaults={'name': company_name}
- )
- if created:
- companies_created += 1
-
- # Create vendors
- vendors_created = 0
- for vendor_id, vendor_name in vendors:
- vendor, created = Vendor.objects.get_or_create(
- vendor_id=vendor_id,
- defaults={'name': vendor_name}
- )
- if created:
- vendors_created += 1
-
- self.stdout.write(f'Created {companies_created} new companies and {vendors_created} new vendors')
-
- def load_items(self, fixtures_path):
- """Load items from CSV files."""
- self.stdout.write('Loading items...')
-
- items = set()
-
- # Extract items from all CSV files
- for csv_file in ['invoices.csv', 'purchase-orders.csv', 'goods-received.csv']:
- csv_path = os.path.join(fixtures_path, csv_file)
- if os.path.exists(csv_path):
- with open(csv_path, 'r') as file:
- reader = csv.DictReader(file)
- for row in reader:
- if 'Item_Code' in row and 'Description' in row:
- items.add((row['Item_Code'], row['Description']))
-
- # Create items
- items_created = 0
- for item_code, description in items:
- item, created = Item.objects.get_or_create(
- item_code=item_code,
- defaults={'description': description}
- )
- if created:
- items_created += 1
-
- self.stdout.write(f'Created {items_created} new items')
-
- def load_purchase_orders(self, fixtures_path):
- """Load purchase orders from CSV."""
- csv_path = os.path.join(fixtures_path, 'purchase-orders.csv')
- if not os.path.exists(csv_path):
- self.stdout.write('purchase-orders.csv not found, skipping...')
- return
-
- self.stdout.write('Loading purchase orders...')
-
- pos_created = 0
- line_items_created = 0
-
- with open(csv_path, 'r') as file:
- reader = csv.DictReader(file)
- for row in reader:
- po_number = row['PO_Number']
-
- # Create PO if it doesn't exist
- try:
- vendor = Vendor.objects.get(vendor_id=row['Vendor_ID'])
- company = Company.objects.get(company_id=row['Company_ID'])
-
- po, created = PurchaseOrder.objects.get_or_create(
- po_number=po_number,
- defaults={
- 'date': datetime.strptime(row['Date'], '%Y-%m-%d').date(),
- 'required_delivery_date': datetime.strptime(row['Required_Delivery_Date'], '%Y-%m-%d').date(),
- 'vendor': vendor,
- 'company': company,
- 'currency': row['Currency'],
- 'status': row['PO_Status'].upper()
- }
- )
-
- if created:
- pos_created += 1
-
- # Create line item if it doesn't exist
- item = Item.objects.get(item_code=row['Item_Code'])
-
- line_item, created = PurchaseOrderLineItem.objects.get_or_create(
- purchase_order=po,
- item=item,
- defaults={
- 'quantity': Decimal(row['Quantity']),
- 'unit_price': Decimal(row['Unit_Price']),
- 'total': Decimal(row['Total'])
- }
- )
-
- if created:
- line_items_created += 1
-
- except (Vendor.DoesNotExist, Company.DoesNotExist, Item.DoesNotExist) as e:
- self.stdout.write(f'Skipping PO {po_number}: {e}')
- continue
-
- self.stdout.write(f'Created {pos_created} purchase orders with {line_items_created} line items')
-
- def load_goods_received(self, fixtures_path):
- """Load goods received from CSV."""
- csv_path = os.path.join(fixtures_path, 'goods-received.csv')
- if not os.path.exists(csv_path):
- self.stdout.write('goods-received.csv not found, skipping...')
- return
-
- self.stdout.write('Loading goods received...')
-
- grs_created = 0
- line_items_created = 0
-
- with open(csv_path, 'r') as file:
- reader = csv.DictReader(file)
- for row in reader:
- gr_number = row['GR_Number']
-
- try:
- vendor = Vendor.objects.get(vendor_id=row['Vendor_ID'])
- company = Company.objects.get(company_id=row['Company_ID'])
-
- # Get PO if exists
- po = None
- if row.get('PO_Number'):
- try:
- po = PurchaseOrder.objects.get(po_number=row['PO_Number'])
- except PurchaseOrder.DoesNotExist:
- pass
-
- gr, created = GoodsReceived.objects.get_or_create(
- gr_number=gr_number,
- defaults={
- 'date': datetime.strptime(row['Date'], '%Y-%m-%d').date(),
- 'purchase_order': po,
- 'vendor': vendor,
- 'company': company,
- 'notes': row.get('Notes', '')
- }
- )
-
- if created:
- grs_created += 1
-
- # Create line item if it doesn't exist
- item = Item.objects.get(item_code=row['Item_Code'])
-
- line_item, created = GoodsReceivedLineItem.objects.get_or_create(
- goods_received=gr,
- item=item,
- defaults={
- 'quantity_ordered': Decimal(row['Quantity_Ordered']),
- 'quantity_received': Decimal(row['Quantity_Received']),
- 'notes': row.get('Notes', '')
- }
- )
-
- if created:
- line_items_created += 1
-
- except (Vendor.DoesNotExist, Company.DoesNotExist, Item.DoesNotExist) as e:
- self.stdout.write(f'Skipping GR {gr_number}: {e}')
- continue
-
- self.stdout.write(f'Created {grs_created} goods received with {line_items_created} line items')
-
- def load_invoices(self, fixtures_path):
- """Load invoices from CSV."""
- csv_path = os.path.join(fixtures_path, 'invoices.csv')
- if not os.path.exists(csv_path):
- self.stdout.write('invoices.csv not found, skipping...')
- return
-
- self.stdout.write('Loading invoices...')
-
- invoices_created = 0
- line_items_created = 0
-
- with open(csv_path, 'r') as file:
- reader = csv.DictReader(file)
- for row in reader:
- invoice_number = row['Invoice_Number']
-
- try:
- vendor = Vendor.objects.get(vendor_id=row['Vendor_ID'])
- company = Company.objects.get(company_id=row['Company_ID'])
-
- invoice, created = Invoice.objects.get_or_create(
- invoice_number=invoice_number,
- defaults={
- 'date': datetime.strptime(row['Date'], '%Y-%m-%d').date(),
- 'due_date': datetime.strptime(row['Due_Date'], '%Y-%m-%d').date(),
- 'po_number': row.get('PO_Number', ''),
- 'gr_number': row.get('GR_Number', ''),
- 'vendor': vendor,
- 'company': company,
- 'currency': row['Currency'],
- 'payment_terms': row['Payment_Terms'],
- 'shipping': Decimal(row.get('Shipping', '0'))
- }
- )
-
- if created:
- invoices_created += 1
-
- # Create line item if it doesn't exist
- item = Item.objects.get(item_code=row['Item_Code'])
-
- line_item, created = InvoiceLineItem.objects.get_or_create(
- invoice=invoice,
- item=item,
- defaults={
- 'quantity': Decimal(row['Quantity']),
- 'unit_price': Decimal(row['Unit_Price']),
- 'total': Decimal(row['Total']),
- 'discount_percent': Decimal(row.get('Discount_Percent', '0')),
- 'discount_amount': Decimal(row.get('Discount_Amount', '0')),
- 'tax_rate': Decimal(row.get('Tax_Rate', '0'))
- }
- )
-
- if created:
- line_items_created += 1
-
- except (Vendor.DoesNotExist, Company.DoesNotExist, Item.DoesNotExist) as e:
- self.stdout.write(f'Skipping Invoice {invoice_number}: {e}')
- continue
-
- self.stdout.write(f'Created {invoices_created} invoices with {line_items_created} line items')
\ No newline at end of file
diff --git a/backend/invoices/migrations/0001_initial.py b/backend/invoices/migrations/0001_initial.py
deleted file mode 100644
index 156c695..0000000
--- a/backend/invoices/migrations/0001_initial.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# Generated by Django 5.0.1 on 2025-06-02 06:37
-
-import django.core.validators
-import django.db.models.deletion
-from decimal import Decimal
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='Company',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('company_id', models.CharField(max_length=10, unique=True)),
- ('name', models.CharField(max_length=255)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ],
- options={
- 'verbose_name_plural': 'Companies',
- },
- ),
- migrations.CreateModel(
- name='Item',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('item_code', models.CharField(max_length=20, unique=True)),
- ('description', models.CharField(max_length=255)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ],
- ),
- migrations.CreateModel(
- name='Vendor',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('vendor_id', models.CharField(max_length=10, unique=True)),
- ('name', models.CharField(max_length=255)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ],
- ),
- migrations.CreateModel(
- name='Invoice',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('invoice_number', models.CharField(max_length=50, unique=True)),
- ('date', models.DateField()),
- ('due_date', models.DateField()),
- ('po_number', models.CharField(blank=True, max_length=50)),
- ('gr_number', models.CharField(blank=True, max_length=50)),
- ('currency', models.CharField(choices=[('USD', 'US Dollar'), ('EUR', 'Euro'), ('GBP', 'British Pound')], default='USD', max_length=3)),
- ('payment_terms', models.CharField(max_length=50)),
- ('sub_total', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
- ('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
- ('tax_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
- ('shipping', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
- ('total_due', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='invoices.company')),
- ('vendor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='invoices.vendor')),
- ],
- options={
- 'ordering': ['-date', '-invoice_number'],
- },
- ),
- migrations.CreateModel(
- name='InvoiceLineItem',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('quantity', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))])),
- ('unit_price', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))])),
- ('total', models.DecimalField(decimal_places=2, max_digits=12)),
- ('discount_percent', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=5)),
- ('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
- ('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=5)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='line_items', to='invoices.invoice')),
- ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item')),
- ],
- options={
- 'unique_together': {('invoice', 'item')},
- },
- ),
- ]
diff --git a/backend/invoices/migrations/__init__.py b/backend/invoices/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/invoices/models.py b/backend/invoices/models.py
deleted file mode 100644
index 81c1a71..0000000
--- a/backend/invoices/models.py
+++ /dev/null
@@ -1,132 +0,0 @@
-from django.db import models
-from django.core.validators import MinValueValidator
-from decimal import Decimal
-
-
-class Company(models.Model):
- """Company model for both vendors and clients."""
- company_id = models.CharField(max_length=10, unique=True)
- name = models.CharField(max_length=255)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- verbose_name_plural = "Companies"
-
- def __str__(self):
- return f"{self.company_id} - {self.name}"
-
-
-class Vendor(models.Model):
- """Vendor model."""
- vendor_id = models.CharField(max_length=10, unique=True)
- name = models.CharField(max_length=255)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- def __str__(self):
- return f"{self.vendor_id} - {self.name}"
-
-
-class Item(models.Model):
- """Item/Product model."""
- item_code = models.CharField(max_length=20, unique=True)
- description = models.CharField(max_length=255)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- def __str__(self):
- return f"{self.item_code} - {self.description}"
-
-
-class Invoice(models.Model):
- """Invoice header model."""
- CURRENCY_CHOICES = [
- ('USD', 'US Dollar'),
- ('EUR', 'Euro'),
- ('GBP', 'British Pound'),
- ]
-
- invoice_number = models.CharField(max_length=50, unique=True)
- date = models.DateField()
- due_date = models.DateField()
- po_number = models.CharField(max_length=50, blank=True)
- gr_number = models.CharField(max_length=50, blank=True)
-
- vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='invoices')
- company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='invoices')
-
- currency = models.CharField(max_length=3, choices=CURRENCY_CHOICES, default='USD')
- payment_terms = models.CharField(max_length=50)
-
- sub_total = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
- discount_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
- tax_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
- shipping = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
- total_due = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- ordering = ['-date', '-invoice_number']
-
- def __str__(self):
- return f"{self.invoice_number} - {self.vendor.name}"
-
- def calculate_totals(self):
- """Calculate invoice totals from line items."""
- line_items = self.line_items.all()
-
- item_total = sum(item.total for item in line_items)
- discount_total = sum(item.discount_amount for item in line_items)
-
- self.sub_total = item_total - discount_total
- self.discount_amount = discount_total
-
- # Tax calculation (could be more sophisticated)
- tax_rates = line_items.values_list('tax_rate', flat=True).distinct()
- if tax_rates:
- # For simplicity, use the first tax rate found
- tax_rate = tax_rates[0] if tax_rates[0] else Decimal('0')
- self.tax_amount = (self.sub_total * tax_rate / 100).quantize(Decimal('0.01'))
-
- self.total_due = self.sub_total + self.tax_amount + self.shipping
- self.save()
-
-
-class InvoiceLineItem(models.Model):
- """Invoice line item model."""
- invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='line_items')
- item = models.ForeignKey(Item, on_delete=models.CASCADE)
-
- quantity = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.01'))])
- unit_price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))])
- total = models.DecimalField(max_digits=12, decimal_places=2)
-
- discount_percent = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.00'))
- discount_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
- tax_rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.00'))
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- unique_together = ['invoice', 'item']
-
- def __str__(self):
- return f"{self.invoice.invoice_number} - {self.item.description}"
-
- def save(self, *args, **kwargs):
- """Calculate totals before saving."""
- # Calculate total before discount
- self.total = self.quantity * self.unit_price
-
- # Calculate discount amount
- if self.discount_percent > 0:
- self.discount_amount = (self.total * self.discount_percent / 100).quantize(Decimal('0.01'))
-
- super().save(*args, **kwargs)
-
- # Update invoice totals
- self.invoice.calculate_totals()
diff --git a/backend/invoices/serializers.py b/backend/invoices/serializers.py
deleted file mode 100644
index 1961855..0000000
--- a/backend/invoices/serializers.py
+++ /dev/null
@@ -1,124 +0,0 @@
-from rest_framework import serializers
-from .models import Company, Vendor, Item, Invoice, InvoiceLineItem
-from datetime import date, timedelta
-
-
-class CompanySerializer(serializers.ModelSerializer):
- class Meta:
- model = Company
- fields = ['id', 'company_id', 'name', 'created_at', 'updated_at']
- read_only_fields = ['id', 'created_at', 'updated_at']
-
-
-class VendorSerializer(serializers.ModelSerializer):
- class Meta:
- model = Vendor
- fields = ['id', 'vendor_id', 'name', 'created_at', 'updated_at']
- read_only_fields = ['id', 'created_at', 'updated_at']
-
-
-class ItemSerializer(serializers.ModelSerializer):
- class Meta:
- model = Item
- fields = ['id', 'item_code', 'description', 'created_at', 'updated_at']
- read_only_fields = ['id', 'created_at', 'updated_at']
-
-
-class InvoiceLineItemSerializer(serializers.ModelSerializer):
- item_code = serializers.CharField(source='item.item_code', read_only=True)
- item_description = serializers.CharField(source='item.description', read_only=True)
-
- class Meta:
- model = InvoiceLineItem
- fields = [
- 'id', 'item', 'item_code', 'item_description',
- 'quantity', 'unit_price', 'total',
- 'discount_percent', 'discount_amount', 'tax_rate',
- 'created_at', 'updated_at'
- ]
- read_only_fields = ['id', 'total', 'discount_amount', 'created_at', 'updated_at']
-
-
-class InvoiceSerializer(serializers.ModelSerializer):
- vendor_name = serializers.CharField(source='vendor.name', read_only=True)
- vendor_id = serializers.CharField(source='vendor.vendor_id', read_only=True)
- company_name = serializers.CharField(source='company.name', read_only=True)
- company_id = serializers.CharField(source='company.company_id', read_only=True)
- line_items = InvoiceLineItemSerializer(many=True, read_only=True)
-
- # Frontend-expected field names
- invoiceNumber = serializers.CharField(source='invoice_number', read_only=True)
- dueDate = serializers.DateField(source='due_date', read_only=True)
- poNumber = serializers.CharField(source='po_number', read_only=True)
- grNumber = serializers.CharField(source='gr_number', read_only=True)
- vendor = serializers.CharField(source='vendor.name', read_only=True)
- amount = serializers.DecimalField(source='total_due', max_digits=12, decimal_places=2, read_only=True)
- status = serializers.SerializerMethodField()
- match = serializers.SerializerMethodField()
-
- class Meta:
- model = Invoice
- fields = [
- 'id', 'invoice_number', 'date', 'due_date',
- 'po_number', 'gr_number',
- 'vendor', 'vendor_id', 'vendor_name',
- 'company', 'company_id', 'company_name',
- 'currency', 'payment_terms',
- 'sub_total', 'discount_amount', 'tax_amount',
- 'shipping', 'total_due',
- 'line_items',
- # Frontend-expected fields
- 'invoiceNumber', 'dueDate', 'poNumber', 'grNumber',
- 'amount', 'status', 'match',
- 'created_at', 'updated_at'
- ]
- read_only_fields = [
- 'id', 'sub_total', 'discount_amount', 'tax_amount', 'total_due',
- 'created_at', 'updated_at'
- ]
-
- def get_status(self, obj):
- """Return a status based on due date and other factors."""
- today = date.today()
-
- if not obj.due_date:
- return "In Approval" # No due date set
- elif obj.due_date < today:
- return "Review" # Overdue
- elif obj.due_date <= today + timedelta(days=7):
- return "In Approval" # Due soon
- else:
- return "Approved" # Not due yet
-
- def get_match(self, obj):
- """Return a match color based on PO/GR linking."""
- if obj.po_number and obj.gr_number:
- return "green" # Fully matched
- elif obj.po_number or obj.gr_number:
- return "yellow" # Partially matched
- else:
- return "red" # No matching
-
-
-class InvoiceCreateSerializer(serializers.ModelSerializer):
- """Serializer for creating invoices with line items."""
- line_items = InvoiceLineItemSerializer(many=True)
-
- class Meta:
- model = Invoice
- fields = [
- 'invoice_number', 'date', 'due_date',
- 'po_number', 'gr_number',
- 'vendor', 'company',
- 'currency', 'payment_terms', 'shipping',
- 'line_items'
- ]
-
- def create(self, validated_data):
- line_items_data = validated_data.pop('line_items')
- invoice = Invoice.objects.create(**validated_data)
-
- for line_item_data in line_items_data:
- InvoiceLineItem.objects.create(invoice=invoice, **line_item_data)
-
- return invoice
\ No newline at end of file
diff --git a/backend/invoices/tests.py b/backend/invoices/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/backend/invoices/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/backend/invoices/views.py b/backend/invoices/views.py
deleted file mode 100644
index a78da97..0000000
--- a/backend/invoices/views.py
+++ /dev/null
@@ -1,106 +0,0 @@
-from django.shortcuts import render
-from rest_framework import viewsets, filters
-from rest_framework.response import Response
-from rest_framework.decorators import action
-from django_filters.rest_framework import DjangoFilterBackend
-from django.db.models import Q
-from .models import Company, Vendor, Item, Invoice, InvoiceLineItem
-from .serializers import (
- CompanySerializer, VendorSerializer, ItemSerializer,
- InvoiceSerializer, InvoiceCreateSerializer, InvoiceLineItemSerializer
-)
-
-
-class CompanyViewSet(viewsets.ModelViewSet):
- """ViewSet for Company model."""
- queryset = Company.objects.all()
- serializer_class = CompanySerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['company_id']
- search_fields = ['company_id', 'name']
- ordering_fields = ['company_id', 'name', 'created_at']
- ordering = ['company_id']
-
-
-class VendorViewSet(viewsets.ModelViewSet):
- """ViewSet for Vendor model."""
- queryset = Vendor.objects.all()
- serializer_class = VendorSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['vendor_id']
- search_fields = ['vendor_id', 'name']
- ordering_fields = ['vendor_id', 'name', 'created_at']
- ordering = ['vendor_id']
-
-
-class ItemViewSet(viewsets.ModelViewSet):
- """ViewSet for Item model."""
- queryset = Item.objects.all()
- serializer_class = ItemSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['item_code']
- search_fields = ['item_code', 'description']
- ordering_fields = ['item_code', 'description', 'created_at']
- ordering = ['item_code']
-
-
-class InvoiceViewSet(viewsets.ModelViewSet):
- """ViewSet for Invoice model."""
- queryset = Invoice.objects.select_related('vendor', 'company').prefetch_related('line_items__item')
- serializer_class = InvoiceSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['vendor', 'company', 'currency', 'po_number']
- search_fields = ['invoice_number', 'vendor__name', 'company__name']
- ordering_fields = ['date', 'invoice_number', 'total_due', 'created_at']
- ordering = ['-date', '-invoice_number']
-
- def get_serializer_class(self):
- """Return appropriate serializer based on action."""
- if self.action == 'create':
- return InvoiceCreateSerializer
- return InvoiceSerializer
-
- @action(detail=False, methods=['get'])
- def summary(self, request):
- """Get invoice summary statistics."""
- queryset = self.filter_queryset(self.get_queryset())
-
- total_invoices = queryset.count()
- total_amount = sum(invoice.total_due for invoice in queryset)
-
- # Group by vendor
- vendor_summary = {}
- for invoice in queryset:
- vendor_name = invoice.vendor.name
- if vendor_name not in vendor_summary:
- vendor_summary[vendor_name] = {
- 'count': 0,
- 'total_amount': 0
- }
- vendor_summary[vendor_name]['count'] += 1
- vendor_summary[vendor_name]['total_amount'] += float(invoice.total_due)
-
- return Response({
- 'total_invoices': total_invoices,
- 'total_amount': total_amount,
- 'vendor_summary': vendor_summary
- })
-
- @action(detail=True, methods=['get'])
- def line_items(self, request, pk=None):
- """Get line items for a specific invoice."""
- invoice = self.get_object()
- line_items = invoice.line_items.select_related('item')
- serializer = InvoiceLineItemSerializer(line_items, many=True)
- return Response(serializer.data)
-
-
-class InvoiceLineItemViewSet(viewsets.ModelViewSet):
- """ViewSet for InvoiceLineItem model."""
- queryset = InvoiceLineItem.objects.select_related('invoice', 'item')
- serializer_class = InvoiceLineItemSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['invoice', 'item']
- search_fields = ['item__item_code', 'item__description']
- ordering_fields = ['quantity', 'unit_price', 'total', 'created_at']
- ordering = ['created_at']
diff --git a/backend/manage.py b/backend/manage.py
deleted file mode 100755
index 931502b..0000000
--- a/backend/manage.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env python
-"""Django's command-line utility for administrative tasks."""
-import os
-import sys
-
-
-def main():
- """Run administrative tasks."""
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'invoice_backend.settings')
- try:
- from django.core.management import execute_from_command_line
- except ImportError as exc:
- raise ImportError(
- "Couldn't import Django. Are you sure it's installed and "
- "available on your PYTHONPATH environment variable? Did you "
- "forget to activate a virtual environment?"
- ) from exc
- execute_from_command_line(sys.argv)
-
-
-if __name__ == '__main__':
- main()
diff --git a/backend/purchase_orders/__init__.py b/backend/purchase_orders/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/purchase_orders/admin.py b/backend/purchase_orders/admin.py
deleted file mode 100644
index 8c38f3f..0000000
--- a/backend/purchase_orders/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/backend/purchase_orders/apps.py b/backend/purchase_orders/apps.py
deleted file mode 100644
index 74f4374..0000000
--- a/backend/purchase_orders/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class PurchaseOrdersConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'purchase_orders'
diff --git a/backend/purchase_orders/migrations/0001_initial.py b/backend/purchase_orders/migrations/0001_initial.py
deleted file mode 100644
index 9382e18..0000000
--- a/backend/purchase_orders/migrations/0001_initial.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# Generated by Django 5.0.1 on 2025-06-02 06:37
-
-import django.core.validators
-import django.db.models.deletion
-from decimal import Decimal
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('invoices', '0001_initial'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='PurchaseOrder',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('po_number', models.CharField(max_length=50, unique=True)),
- ('date', models.DateField()),
- ('required_delivery_date', models.DateField()),
- ('currency', models.CharField(choices=[('USD', 'US Dollar'), ('EUR', 'Euro'), ('GBP', 'British Pound')], default='USD', max_length=3)),
- ('status', models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('COMPLETED', 'Completed'), ('CANCELLED', 'Cancelled')], default='PENDING', max_length=20)),
- ('total_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchase_orders', to='invoices.company')),
- ('vendor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchase_orders', to='invoices.vendor')),
- ],
- options={
- 'ordering': ['-date', '-po_number'],
- },
- ),
- migrations.CreateModel(
- name='PurchaseOrderLineItem',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('quantity', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))])),
- ('unit_price', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))])),
- ('total', models.DecimalField(decimal_places=2, max_digits=12)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item')),
- ('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='line_items', to='purchase_orders.purchaseorder')),
- ],
- options={
- 'unique_together': {('purchase_order', 'item')},
- },
- ),
- ]
diff --git a/backend/purchase_orders/migrations/__init__.py b/backend/purchase_orders/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/purchase_orders/models.py b/backend/purchase_orders/models.py
deleted file mode 100644
index 45c3e3d..0000000
--- a/backend/purchase_orders/models.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from django.db import models
-from django.core.validators import MinValueValidator
-from decimal import Decimal
-from invoices.models import Company, Vendor, Item
-
-
-class PurchaseOrder(models.Model):
- """Purchase Order header model."""
- STATUS_CHOICES = [
- ('PENDING', 'Pending'),
- ('APPROVED', 'Approved'),
- ('REJECTED', 'Rejected'),
- ('COMPLETED', 'Completed'),
- ('CANCELLED', 'Cancelled'),
- ]
-
- CURRENCY_CHOICES = [
- ('USD', 'US Dollar'),
- ('EUR', 'Euro'),
- ('GBP', 'British Pound'),
- ]
-
- po_number = models.CharField(max_length=50, unique=True)
- date = models.DateField()
- required_delivery_date = models.DateField()
-
- vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='purchase_orders')
- company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='purchase_orders')
-
- currency = models.CharField(max_length=3, choices=CURRENCY_CHOICES, default='USD')
- status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
-
- total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- ordering = ['-date', '-po_number']
-
- def __str__(self):
- return f"{self.po_number} - {self.vendor.name}"
-
- def calculate_total(self):
- """Calculate total amount from line items."""
- line_items = self.line_items.all()
- self.total_amount = sum(item.total for item in line_items)
- self.save()
-
-
-class PurchaseOrderLineItem(models.Model):
- """Purchase Order line item model."""
- purchase_order = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name='line_items')
- item = models.ForeignKey(Item, on_delete=models.CASCADE)
-
- quantity = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.01'))])
- unit_price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))])
- total = models.DecimalField(max_digits=12, decimal_places=2)
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- unique_together = ['purchase_order', 'item']
-
- def __str__(self):
- return f"{self.purchase_order.po_number} - {self.item.description}"
-
- def save(self, *args, **kwargs):
- """Calculate total before saving."""
- self.total = self.quantity * self.unit_price
- super().save(*args, **kwargs)
-
- # Update PO total
- self.purchase_order.calculate_total()
diff --git a/backend/purchase_orders/serializers.py b/backend/purchase_orders/serializers.py
deleted file mode 100644
index 1a5aff4..0000000
--- a/backend/purchase_orders/serializers.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from rest_framework import serializers
-from .models import PurchaseOrder, PurchaseOrderLineItem
-from invoices.serializers import CompanySerializer, VendorSerializer, ItemSerializer
-
-
-class PurchaseOrderLineItemSerializer(serializers.ModelSerializer):
- item_code = serializers.CharField(source='item.item_code', read_only=True)
- item_description = serializers.CharField(source='item.description', read_only=True)
-
- class Meta:
- model = PurchaseOrderLineItem
- fields = [
- 'id', 'item', 'item_code', 'item_description',
- 'quantity', 'unit_price', 'total',
- 'created_at', 'updated_at'
- ]
- read_only_fields = ['id', 'total', 'created_at', 'updated_at']
-
-
-class PurchaseOrderSerializer(serializers.ModelSerializer):
- vendor_name = serializers.CharField(source='vendor.name', read_only=True)
- vendor_id = serializers.CharField(source='vendor.vendor_id', read_only=True)
- company_name = serializers.CharField(source='company.name', read_only=True)
- company_id = serializers.CharField(source='company.company_id', read_only=True)
- line_items = PurchaseOrderLineItemSerializer(many=True, read_only=True)
-
- # Frontend-expected field names
- poNumber = serializers.CharField(source='po_number', read_only=True)
- vendorName = serializers.CharField(source='vendor.name', read_only=True)
- companyName = serializers.CharField(source='company.name', read_only=True)
- totalAmount = serializers.DecimalField(source='total_amount', max_digits=12, decimal_places=2, read_only=True)
-
- class Meta:
- model = PurchaseOrder
- fields = [
- 'id', 'po_number', 'date', 'required_delivery_date',
- 'vendor', 'vendor_id', 'vendor_name',
- 'company', 'company_id', 'company_name',
- 'currency', 'status', 'total_amount',
- 'line_items',
- # Frontend-expected fields
- 'poNumber', 'vendorName', 'companyName', 'totalAmount',
- 'created_at', 'updated_at'
- ]
- read_only_fields = ['id', 'total_amount', 'created_at', 'updated_at']
-
-
-class PurchaseOrderCreateSerializer(serializers.ModelSerializer):
- """Serializer for creating purchase orders with line items."""
- line_items = PurchaseOrderLineItemSerializer(many=True)
-
- class Meta:
- model = PurchaseOrder
- fields = [
- 'po_number', 'date', 'required_delivery_date',
- 'vendor', 'company', 'currency', 'status',
- 'line_items'
- ]
-
- def create(self, validated_data):
- line_items_data = validated_data.pop('line_items')
- purchase_order = PurchaseOrder.objects.create(**validated_data)
-
- for line_item_data in line_items_data:
- PurchaseOrderLineItem.objects.create(purchase_order=purchase_order, **line_item_data)
-
- return purchase_order
\ No newline at end of file
diff --git a/backend/purchase_orders/tests.py b/backend/purchase_orders/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/backend/purchase_orders/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/backend/purchase_orders/views.py b/backend/purchase_orders/views.py
deleted file mode 100644
index 3a9713a..0000000
--- a/backend/purchase_orders/views.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from django.shortcuts import render
-from rest_framework import viewsets, filters
-from rest_framework.response import Response
-from rest_framework.decorators import action
-from django_filters.rest_framework import DjangoFilterBackend
-from .models import PurchaseOrder, PurchaseOrderLineItem
-from .serializers import (
- PurchaseOrderSerializer, PurchaseOrderCreateSerializer, PurchaseOrderLineItemSerializer
-)
-
-
-class PurchaseOrderViewSet(viewsets.ModelViewSet):
- """ViewSet for PurchaseOrder model."""
- queryset = PurchaseOrder.objects.select_related('vendor', 'company').prefetch_related('line_items__item')
- serializer_class = PurchaseOrderSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['vendor', 'company', 'currency', 'status']
- search_fields = ['po_number', 'vendor__name', 'company__name']
- ordering_fields = ['date', 'po_number', 'total_amount', 'created_at']
- ordering = ['-date', '-po_number']
-
- def get_serializer_class(self):
- """Return appropriate serializer based on action."""
- if self.action == 'create':
- return PurchaseOrderCreateSerializer
- return PurchaseOrderSerializer
-
- @action(detail=False, methods=['get'])
- def summary(self, request):
- """Get purchase order summary statistics."""
- queryset = self.filter_queryset(self.get_queryset())
-
- total_pos = queryset.count()
- total_amount = sum(po.total_amount for po in queryset)
-
- # Group by status
- status_summary = {}
- for po in queryset:
- status = po.status
- if status not in status_summary:
- status_summary[status] = {
- 'count': 0,
- 'total_amount': 0
- }
- status_summary[status]['count'] += 1
- status_summary[status]['total_amount'] += float(po.total_amount)
-
- return Response({
- 'total_purchase_orders': total_pos,
- 'total_amount': total_amount,
- 'status_summary': status_summary
- })
-
-
-class PurchaseOrderLineItemViewSet(viewsets.ModelViewSet):
- """ViewSet for PurchaseOrderLineItem model."""
- queryset = PurchaseOrderLineItem.objects.select_related('purchase_order', 'item')
- serializer_class = PurchaseOrderLineItemSerializer
- filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
- filterset_fields = ['purchase_order', 'item']
- search_fields = ['item__item_code', 'item__description']
- ordering_fields = ['quantity', 'unit_price', 'total', 'created_at']
- ordering = ['created_at']
diff --git a/backend/railway.json b/backend/railway.json
deleted file mode 100644
index 1d88f85..0000000
--- a/backend/railway.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "$schema": "https://railway.app/railway.schema.json",
- "build": {
- "builder": "NIXPACKS"
- },
- "deploy": {
- "startCommand": "python manage.py migrate && python manage.py collectstatic --noinput && gunicorn invoice_backend.wsgi:application --bind 0.0.0.0:$PORT",
- "healthcheckPath": "/api/health/",
- "healthcheckTimeout": 100,
- "restartPolicyType": "ON_FAILURE",
- "restartPolicyMaxRetries": 10
- }
-}
\ No newline at end of file
diff --git a/backend/requirements.txt b/backend/requirements.txt
index fd0fba6..204471f 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1,17 +1,10 @@
-Django==5.0.1
-djangorestframework==3.14.0
-django-cors-headers==4.3.1
-django-environ==0.11.2
-django-filter==23.5
-psycopg2-binary==2.9.9
-celery==5.3.4
-redis==5.0.1
-gunicorn==21.2.0
-anthropic>=0.34.0
-opencv-python==4.11.0.86
-Pillow==10.1.0
-PyMuPDF==1.26.0
-numpy==2.2.6
-python-dotenv==1.1.0
+anthropic
+pymupdf
+opencv-python
+numpy
+python-dotenv
boto3
-whitenoise==6.6.0
\ No newline at end of file
+pillow
+fastapi
+uvicorn
+python-multipart
\ No newline at end of file
diff --git a/backend/start.sh b/backend/start.sh
new file mode 100755
index 0000000..9d14e3f
--- /dev/null
+++ b/backend/start.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+# Install dependencies if needed
+echo "Installing Python dependencies..."
+pip install -r requirements.txt
+
+# Start the FastAPI server
+echo "Starting FastAPI server at http://localhost:8001"
+uvicorn api:app --reload --host 0.0.0.0 --port 8001
\ No newline at end of file
diff --git a/frontend/components.json b/components.json
similarity index 100%
rename from frontend/components.json
rename to components.json
diff --git a/frontend/components/goods-received-table.tsx b/components/goods-received-table.tsx
similarity index 54%
rename from frontend/components/goods-received-table.tsx
rename to components/goods-received-table.tsx
index 6f35e98..8f4eecb 100644
--- a/frontend/components/goods-received-table.tsx
+++ b/components/goods-received-table.tsx
@@ -5,49 +5,18 @@ import { Badge } from "@/components/ui/badge"
import { useEffect, useState } from "react"
import type { GoodsReceived } from "@/lib/data-utils"
-const BACKEND_URL = process.env.NODE_ENV === 'development'
- ? (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000')
- : (process.env.NEXT_PUBLIC_API_URL || 'https://invoice-processing-poc-production.up.railway.app')
-
export default function GoodsReceivedTable() {
const [goodsReceived, setGoodsReceived] = useState([])
const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
useEffect(() => {
async function fetchGoodsReceived() {
try {
- console.log(`Fetching goods received directly from Railway: ${BACKEND_URL}/api/goods-received/`)
-
- const response = await fetch(`${BACKEND_URL}/api/goods-received/`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- },
- signal: AbortSignal.timeout(30000),
- mode: 'cors'
- })
-
- console.log(`Response status: ${response.status}`)
-
- if (!response.ok) {
- throw new Error(`Railway API returned ${response.status}`)
- }
-
+ const response = await fetch("/api/goods-received")
const data = await response.json()
- console.log(`Successfully fetched data from Railway:`, {
- count: data.count,
- resultsLength: data.results?.length
- })
-
- // Transform Django REST Framework pagination format to match frontend expectations
- const transformedData = data.results || data
- setGoodsReceived(transformedData)
-
+ setGoodsReceived(data)
} catch (error) {
- console.error("Error fetching goods received from Railway:", error)
- setError(error instanceof Error ? error.message : String(error))
+ console.error("Error fetching goods received:", error)
} finally {
setLoading(false)
}
@@ -57,25 +26,11 @@ export default function GoodsReceivedTable() {
}, [])
if (loading) {
- return
Loading goods receipt notes...
- }
-
- if (error) {
- return (
-
-
Error loading goods received: {error}
-
-
- )
+ return
Loading goods received notes...
}
if (goodsReceived.length === 0) {
- return
No goods receipt notes found.
+ return
No goods received notes found.
}
return (
diff --git a/frontend/components/invoice-table.tsx b/components/invoice-table.tsx
similarity index 60%
rename from frontend/components/invoice-table.tsx
rename to components/invoice-table.tsx
index 1d09cf9..53b8695 100644
--- a/frontend/components/invoice-table.tsx
+++ b/components/invoice-table.tsx
@@ -6,50 +6,18 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { useEffect, useState } from "react"
import type { Invoice } from "@/lib/data-utils"
-const BACKEND_URL = process.env.NODE_ENV === 'development'
- ? (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000')
- : (process.env.NEXT_PUBLIC_API_URL || 'https://invoice-processing-poc-production.up.railway.app')
-
export default function InvoiceTable() {
const [invoices, setInvoices] = useState([])
const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
useEffect(() => {
async function fetchInvoices() {
try {
- console.log(`Fetching invoices directly from Railway: ${BACKEND_URL}/api/invoices/`)
-
- const response = await fetch(`${BACKEND_URL}/api/invoices/`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- },
- // Add timeout and CORS mode
- signal: AbortSignal.timeout(30000),
- mode: 'cors'
- })
-
- console.log(`Response status: ${response.status}`)
-
- if (!response.ok) {
- throw new Error(`Railway API returned ${response.status}`)
- }
-
+ const response = await fetch("/api/invoices")
const data = await response.json()
- console.log(`Successfully fetched data from Railway:`, {
- count: data.count,
- resultsLength: data.results?.length
- })
-
- // Transform Django REST Framework pagination format to match frontend expectations
- const transformedData = data.results || data
- setInvoices(transformedData)
-
+ setInvoices(data)
} catch (error) {
- console.error("Error fetching invoices from Railway:", error)
- setError(error instanceof Error ? error.message : String(error))
+ console.error("Error fetching invoices:", error)
} finally {
setLoading(false)
}
@@ -62,20 +30,6 @@ export default function InvoiceTable() {
return