Skip to content

Architecture

Technical architecture of the JDX Field PWA.

System Architecture

flowchart TB
    subgraph "Client Layer"
        Browser[Mobile Browser]
        SW[Service Worker]
        Cache[(Browser Cache)]
    end

    subgraph "Application Layer"
        Flask[Flask App]
        BP1[auth Blueprint]
        BP2[jobs Blueprint]
        BP3[sales Blueprint]
        BP4[crm Blueprint]
        BP5[inventory Blueprint]
        BP6[sms Blueprint]
    end

    subgraph "Service Layer"
        OdooAPI[OdooAPI Service]
        S3Service[S3Service]
        TZService[Timezone Service]
    end

    subgraph "External Services"
        Odoo[Odoo 15 ERP]
        S3[AWS S3]
    end

    Browser --> SW
    SW --> Cache
    Browser --> Flask
    Flask --> BP1 & BP2 & BP3 & BP4 & BP5 & BP6
    BP1 & BP2 & BP3 & BP4 & BP5 & BP6 --> OdooAPI
    BP2 & BP3 --> S3Service
    OdooAPI --> Odoo
    S3Service --> S3

Directory Structure

pwa/
├── app/
│   ├── __init__.py          # Flask app factory
│   ├── config.py             # Configuration class
│   ├── routes/
│   │   ├── auth.py           # Authentication routes
│   │   ├── jobs.py           # FSM job routes
│   │   ├── sales.py          # Sales & quotes routes
│   │   ├── crm.py            # CRM pipeline routes
│   │   ├── inventory.py      # Stock picking routes
│   │   ├── dashboard.py      # Dashboard routes
│   │   ├── customers.py      # Customer routes
│   │   └── sms.py            # SMS API routes
│   ├── services/
│   │   ├── odoo_api.py       # Odoo JSON-RPC/REST client
│   │   ├── s3_service.py     # AWS S3 client
│   │   └── timezone.py       # Timezone utilities
│   ├── templates/            # Jinja2 templates
│   └── static/
│       ├── css/app.css       # Application styles
│       ├── js/
│       │   ├── app.js        # Main JavaScript
│       │   ├── signature.js  # Signature canvas
│       │   ├── sms.js        # SMS modal
│       │   └── form-draft.js # Form draft saving
│       ├── sw.js             # Service Worker
│       ├── manifest.json     # PWA manifest
│       └── icons/            # App icons
├── run.py                    # Application entry point
├── requirements.txt          # Python dependencies
├── Dockerfile               # Container definition
└── .env.example             # Environment template

Flask Blueprints

The application uses Flask Blueprints for modular organization:

Blueprint URL Prefix File Description
auth / routes/auth.py Login, logout, session
jobs / routes/jobs.py FSM orders, signatures
sales / routes/sales.py Sales orders, quotes
crm / routes/crm.py CRM pipeline
inventory /inventory routes/inventory.py Stock pickings
dashboard / routes/dashboard.py Admin dashboard
customers / routes/customers.py Customer lookup
sms / routes/sms.py SMS API

Odoo API Integration

The PWA communicates with Odoo using two methods:

1. REST API (with JWT/API Key)

Used for simple CRUD operations:

# GET request with API Key
headers = {'X-API-Key': api_key}
response = requests.get(f"{base_url}/restapi/1.0/object/fsm.order", headers=headers)

2. JSON-RPC (Session-based)

Used for complex operations and custom field updates:

# JSON-RPC call
payload = {
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
        "model": "fsm.order",
        "method": "write",
        "args": [[record_id], {"x_custom_field": value}],
        "kwargs": {}
    }
}
response = session.post(f"{base_url}/web/dataset/call_kw/fsm.order/write", json=payload)

Authentication Flow

sequenceDiagram
    participant User
    participant Flask
    participant Odoo

    User->>Flask: POST /login (username, password)
    Flask->>Odoo: /web/session/authenticate
    Odoo-->>Flask: {uid, session_id, name}
    Flask->>Flask: Store in Flask session
    Flask-->>User: Redirect to /jobs/active

Service Worker

The PWA uses a service worker (sw.js) for offline support with intelligent caching:

Caching Strategies

Resource Type Strategy TTL Description
Static files (/static/*) Cache-first 30 days CSS, JS, icons
API GET (/api/*) Network-first 1 hour Data requests
HTML pages Network-first - Falls back to offline page
POST/PUT/DELETE Network-only - Never cached

Cache Version Management

const CACHE_VERSION = 'v3';
const STATIC_CACHE = `jdx-static-${CACHE_VERSION}`;
const API_CACHE = `jdx-api-${CACHE_VERSION}`;

Pre-cached Assets

const STATIC_ASSETS = [
    '/',
    '/static/css/app.css',
    '/static/js/app.js',
    '/static/manifest.json',
    '/static/icons/icon-192x192.png',
    '/static/icons/icon-512x512.png',
    '/offline'
];

Offline Fallback

When network is unavailable for HTML pages:

  1. Service worker intercepts the request
  2. Returns cached /offline page
  3. User sees friendly offline message

S3 Integration

AWS S3 is used for storing:

  • Signatures: signatures/fsm_{id}_{timestamp}.png
  • Quote Photos: quotes/{order_id}/photos/{filename}.jpg
  • Quote Videos: quotes/{order_id}/videos/{filename}.mp4

Upload Flow

sequenceDiagram
    participant User
    participant Flask
    participant S3
    participant Odoo

    User->>Flask: POST signature (base64)
    Flask->>S3: put_object()
    S3-->>Flask: Success
    Flask->>Odoo: Update x_signature_url
    Odoo-->>Flask: OK
    Flask-->>User: {success: true}

Pre-signed URLs

For secure access to S3 objects:

url = s3_client.generate_presigned_url(
    'get_object',
    Params={'Bucket': bucket, 'Key': key},
    ExpiresIn=3600  # 1 hour
)

Session Management

Flask Session Configuration

class Config:
    PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
    SESSION_COOKIE_SECURE = True  # Production only
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = 'Lax'

Session Data Stored

Key Description
user_id Odoo user ID (res.users)
username Login username
user_name Display name
partner_id Partner ID for activity attribution

CSRF Protection

The app uses Flask-WTF for CSRF protection:

from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
csrf.init_app(app)

API endpoints that need to bypass CSRF (called from JavaScript):

@bp.route('/api/jobs/<int:fsm_id>/signature', methods=['POST'])
@csrf.exempt
@login_required
def submit_signature(fsm_id):
    ...

Activity Attribution

PWA actions are properly attributed to users in Odoo's activity log:

def write_as_user(self, model, record_id, values, author_partner_id):
    # Get message IDs before write
    before_ids = self.get_record_message_ids(model, record_id)

    # Perform write
    self._jsonrpc_call(model, 'write', [[record_id], values], {})

    # Fix author on tracking messages
    self.fix_tracking_author(model, record_id, before_ids, author_partner_id)

Error Handling

All routes follow a consistent error handling pattern:

@bp.route('/jobs/<int:fsm_id>')
@login_required
def detail(fsm_id):
    try:
        job = odoo_api.get_fsm_order(fsm_id)
        if not job:
            abort(404)
        return render_template('job_detail.html', job=job)
    except Exception as e:
        return render_template('jobs_active.html', jobs=[], error=str(e))

Dependencies

flask>=3.0.0
flask-wtf>=1.2.0
python-dotenv>=1.0.0
requests>=2.31.0
boto3>=1.34.0
gunicorn>=21.0.0
pytz>=2024.1