npm package: @kodeme-io/next-core-odoo-api
Description: Odoo adapter implementing @kodeme-io/next-core-client interfaces with full JSON-RPC support
Author: ABC Food
Latest version: 0.8.4

# @kodeme-io/next-core-odoo-api

**Backend-agnostic Odoo adapter** implementing `@kodeme-io/next-core-client` interfaces with full JSON-RPC support.

## Features

- ✅ **Backend Agnostic** - Implements standard IDataClient and IAuthClient interfaces
- ✅ **Easy Backend Switching** - Can migrate to FastAPI/GraphQL without code changes
- ✅ **Pure JSON-RPC** - Direct Odoo API calls, no custom REST endpoints
- ✅ **Zero Dependencies** - Lightweight, only depends on @kodeme-io/next-core-client
- ✅ **Type Safety** - Full TypeScript support with generic types
- ✅ **Auto Filter Conversion** - Generic filters → Odoo domains automatically
- ✅ **CRUD Operations** - Full create, read, update, delete support
- ✅ **Session Management** - Persistent authentication with auto-refresh
- ✅ **Error Handling** - Comprehensive error handling with ClientError

## Installation

```bash
npm install @kodeme-io/next-core-client @kodeme-io/next-core-odoo-api
```

## Quick Start

### 1. Create OdooAdapter Instance

```typescript
import { OdooAdapter } from '@kodeme-io/next-core-odoo-api'

const odoo = new OdooAdapter({
  url: 'https://your-odoo-server.com',
  database: 'production'
})
```

### 2. Authentication

```typescript
// Login
const authResult = await odoo.login('user@example.com', 'password')
console.log('Logged in as:', authResult.user.name)

// Check session
const isValid = await odoo.checkSession()

// Logout
await odoo.logout()
```

### 3. CRUD Operations

```typescript
// Search with generic filters (no Odoo domain syntax needed!)
const partners = await odoo.searchRead({
  model: 'res.partner',
  filters: [
    { field: 'is_company', operator: 'eq', value: true },
    { field: 'country_id', operator: 'in', value: [1, 2, 3] }
  ],
  fields: ['id', 'name', 'email'],
  limit: 20,
  orderBy: [{ field: 'name', direction: 'asc' }]
})

// Create
const partnerId = await odoo.create('res.partner', {
  name: 'ABC Corporation',
  email: 'contact@abc.com'
})

// Update
await odoo.update('res.partner', partnerId, {
  phone: '+1234567890'
})

// Delete
await odoo.delete('res.partner', partnerId)
```

### CRUD Operations

#### Search & Read

```typescript
// Get all customers
const customers = await odoo.searchRead(
  'res.partner',
  [['is_company', '=', true]],
  ['name', 'email', 'phone']
)

// With pagination
const page1 = await odoo.searchRead(
  'res.partner',
  [],
  ['name'],
  { limit: 10, offset: 0, order: 'name ASC' }
)
```

#### Search (IDs only)

```typescript
// Get customer IDs
const customerIds = await odoo.search(
  'res.partner',
  [['customer_type', '=', 'canvas']],
  { limit: 100 }
)
// Returns: [1, 2, 3, ...]
```

#### Read

```typescript
// Read specific records
const customers = await odoo.read(
  'res.partner',
  [1, 2, 3],
  ['name', 'email', 'phone']
)
```

#### Create

```typescript
// Create new record
const customerId = await odoo.create('res.partner', {
  name: 'New Customer',
  email: 'customer@example.com',
  phone: '+1234567890',
  customer_type: 'canvas'
})
// Returns: new record ID (e.g., 123)
```

#### Update (Write)

```typescript
// Update existing records
const success = await odoo.write(
  'res.partner',
  [123],
  { phone: '+0987654321' }
)
// Returns: true if successful
```

#### Delete (Unlink)

```typescript
// Delete records
const success = await odoo.unlink('res.partner', [123, 456])
// Returns: true if successful
```

### Call Custom Methods

```typescript
// Call any Odoo model method
const result = await odoo.callMethod(
  'sale.order',
  'action_confirm',
  [orderId]
)

// With keyword arguments
const result = await odoo.callMethod(
  'res.partner',
  'mobile_search',
  [],
  { query: 'ABC', limit: 10 }
)
```

## Odoo Domains

Domains use Odoo's Polish notation syntax:

```typescript
// Simple conditions
const domain = [['name', '=', 'ABC']]

// Multiple conditions (AND)
const domain = [
  ['name', 'ilike', 'ABC'],
  ['active', '=', true]
]

// OR conditions
const domain = [
  '|',
  ['name', 'ilike', 'ABC'],
  ['email', 'ilike', 'ABC']
]

// Complex nested conditions
const domain = [
  '&',
  ['active', '=', true],
  '|',
  ['customer_type', '=', 'canvas'],
  ['customer_type', '=', 'taking_order']
]
```

### Domain Operators

- `=`, `!=` - Equals, not equals
- `>`, `>=`, `<`, `<=` - Comparisons
- `like`, `ilike` - Pattern matching (case-sensitive/insensitive)
- `in`, `not in` - List membership
- `=?` - Unset or equals (for optional filters)

## OCA Addon Support

### Operating Unit (Multi-Branch)

```typescript
// Filter by operating unit
const customers = await odoo.searchRead(
  'res.partner',
  [['operating_unit_id', '=', 5]],
  ['name', 'operating_unit_id']
)

// Get user's operating units
const user = await odoo.read('res.users', [userId], [
  'operating_unit_ids',
  'default_operating_unit_id'
])
```

### Tier Validation (Approval Workflow)

```typescript
// Check validation status
const customer = await odoo.searchRead(
  'res.partner',
  [['id', '=', customerId]],
  ['name', 'validated', 'need_validation', 'validation_status']
)

// Request validation
await odoo.callMethod('res.partner', 'request_validation', [customerId])

// Approve/Reject
await odoo.callMethod('tier.validation', 'validate_tier', [reviewId])
await odoo.callMethod('tier.validation', 'reject_tier', [reviewId])
```

## SFA-Specific Methods

The client includes helper methods for common SFA operations:

```typescript
// Get visits by date range
const visits = await odoo.getVisitsByDate(userId, '2024-01-01', '2024-01-31')

// Get customers by type
const canvasCustomers = await odoo.getCustomersByType('canvas')
const allCustomers = await odoo.getCustomersByType()

// Create visit
const visitId = await odoo.createVisit({
  partner_id: 123,
  user_id: userId,
  visit_date: '2024-01-15',
  visit_mode: 'canvas',
  state: 'planned'
})

// Update visit
await odoo.updateVisit(visitId, {
  state: 'completed',
  check_out_time: '2024-01-15 16:30:00'
})

// Upload photo
await odoo.uploadPhoto({
  visit_id: visitId,
  photo_type: 'checkin',
  file_data: base64String,
  file_name: 'checkin.jpg',
  gps_latitude: -6.2088,
  gps_longitude: 106.8456
})

// Validate location (geofence check)
const isValid = await odoo.validateLocation(
  partnerId,
  -6.2088,
  106.8456
)

// Mobile sync
const syncData = await odoo.mobileSync('2024-01-15T10:00:00Z')
```

## Error Handling

```typescript
import { OdooJSONRPCError } from '@next-odoo/odoo-api'

try {
  await odoo.create('res.partner', { name: 'Test' })
} catch (error) {
  if (error instanceof OdooJSONRPCError) {
    console.error('Odoo Error:', {
      code: error.code,
      message: error.message,
      data: error.data
    })
  } else {
    console.error('Network Error:', error)
  }
}
```

## Session Management

```typescript
// Use existing session
const odoo = createOdooClient({
  url: 'https://odoo.yourdomain.com',
  database: 'production_db',
  sessionId: existingSessionId
})

// Get session ID
const sessionId = odoo.getSessionId()

// Set session ID
odoo.setSessionId(newSessionId)

// Check if authenticated
if (odoo.isAuthenticated()) {
  // Make API calls
}
```

## Environment Variables

```env
NEXT_PUBLIC_ODOO_URL=https://odoo.yourdomain.com
NEXT_PUBLIC_ODOO_DB=production_db
```

## API Routes Example

```typescript
// app/api/customers/route.ts
import { createOdooClient } from '@next-odoo/odoo-api'
import { NextResponse } from 'next/server'

export async function GET() {
  const odoo = createOdooClient({
    url: process.env.ODOO_URL!,
    database: process.env.ODOO_DB!,
    sessionId: 'user-session-id' // From request headers/cookies
  })

  const customers = await odoo.searchRead(
    'res.partner',
    [['is_company', '=', true]],
    ['name', 'email']
  )

  return NextResponse.json(customers)
}
```

## Best Practices

1. **Use environment variables** for Odoo credentials
2. **Reuse client instances** - Create once, use throughout app
3. **Handle errors** - Always wrap calls in try/catch
4. **Specify fields** - Only request fields you need
5. **Use pagination** - For large result sets, use limit/offset
6. **Session persistence** - Store and reuse sessionId
7. **Type safety** - Leverage TypeScript types

## License

MIT