@kodeme-io/next-core-partner
v0.3.2
Published
Partner/Customer management module for Next.js + Odoo applications
Maintainers
Readme
@next-odoo/partner
Partner/Customer management module for Next.js + Odoo applications with Canvas pipeline, NOO workflow, and approval management.
Features
- ✅ Customer Management - CRUD operations for partners/customers
- ✅ Canvas Pipeline - Prospect to customer conversion with stages
- ✅ NOO Workflow - New Outlet Opening application process
- ✅ Approval System - Tier validation for taking_order customers
- ✅ React Query Hooks - Optimistic updates and caching
- ✅ Type Safety - Full TypeScript support
- ✅ Operating Unit - Multi-branch/region filtering
- ✅ Search - Fuzzy search by name, ref, or mobile
Installation
pnpm add @next-odoo/partner @next-odoo/odoo-api @tanstack/react-queryUsage
Setup
import { createOdooClient } from '@next-odoo/odoo-api'
import { createPartnerAPI } from '@next-odoo/partner'
// Create Odoo client
const odoo = createOdooClient({
url: process.env.NEXT_PUBLIC_ODOO_URL!,
database: process.env.NEXT_PUBLIC_ODOO_DB!,
sessionId: userSessionId
})
// Create Partner API
const partnerAPI = createPartnerAPI(odoo)Query Hooks
Get All Customers
import { usePartners } from '@next-odoo/partner'
export function CustomerList() {
const { data: customers, isLoading, error } = usePartners(partnerAPI, {
domain: [['active', '=', true]],
limit: 50,
order: 'name ASC'
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{customers?.map(customer => (
<li key={customer.id}>{customer.name}</li>
))}
</ul>
)
}Get Single Customer
import { usePartner } from '@next-odoo/partner'
export function CustomerDetail({ id }: { id: number }) {
const { data: customer } = usePartner(partnerAPI, id)
if (!customer) return null
return (
<div>
<h1>{customer.name}</h1>
<p>Email: {customer.email}</p>
<p>Phone: {customer.mobile}</p>
<p>Type: {customer.customer_type}</p>
</div>
)
}Get Canvas Customers
import { useCanvasCustomers } from '@next-odoo/partner'
export function CanvasDashboard() {
const { data: canvasCustomers } = useCanvasCustomers(partnerAPI)
return (
<div>
<h2>Canvas Customers ({canvasCustomers?.length})</h2>
{/* Render canvas pipeline */}
</div>
)
}Get Prospects
import { useProspects } from '@next-odoo/partner'
export function ProspectList() {
const { data: prospects } = useProspects(partnerAPI)
return <div>Total Prospects: {prospects?.length}</div>
}Get Customers Pending Approval
import { usePendingApproval } from '@next-odoo/partner'
export function ApprovalQueue() {
const { data: pending } = usePendingApproval(partnerAPI)
return (
<div>
<h2>Pending Approval ({pending?.length})</h2>
{pending?.map(customer => (
<CustomerApprovalCard key={customer.id} customer={customer} />
))}
</div>
)
}Get Canvas Stages
import { useCanvasStages } from '@next-odoo/partner'
export function CanvasPipeline() {
const { data: stages } = useCanvasStages(partnerAPI)
return (
<div className="flex gap-4">
{stages?.map(stage => (
<div key={stage.id} className="flex-1">
<h3>{stage.name}</h3>
{/* Render customers in this stage */}
</div>
))}
</div>
)
}Mutation Hooks
Create Customer
import { useCreatePartner } from '@next-odoo/partner'
export function CreateCustomerForm() {
const createCustomer = useCreatePartner(partnerAPI)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
const customerId = await createCustomer.mutateAsync({
name: 'New Customer',
email: '[email protected]',
mobile: '+1234567890',
customer_type: 'canvas',
is_canvas_customer: true
})
console.log('Created customer:', customerId)
} catch (error) {
console.error('Failed to create customer:', error)
}
}
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={createCustomer.isPending}>
{createCustomer.isPending ? 'Creating...' : 'Create Customer'}
</button>
</form>
)
}Update Customer
import { useUpdatePartner } from '@next-odoo/partner'
export function EditCustomer({ customerId }: { customerId: number }) {
const updateCustomer = useUpdatePartner(partnerAPI)
const handleUpdate = async () => {
await updateCustomer.mutateAsync({
id: customerId,
updates: {
phone: '+0987654321',
email: '[email protected]'
}
})
}
return <button onClick={handleUpdate}>Update Customer</button>
}Convert to Canvas
import { useConvertToCanvas } from '@next-odoo/partner'
export function ConvertButton({ customerId, initialStageId }: Props) {
const convertToCanvas = useConvertToCanvas(partnerAPI)
const handleConvert = async () => {
await convertToCanvas.mutateAsync({
customerId,
stageId: initialStageId
})
}
return (
<button onClick={handleConvert}>
Convert to Canvas
</button>
)
}Move to Stage
import { useMoveToStage } from '@next-odoo/partner'
export function MoveStageButton({ customerId, newStageId }: Props) {
const moveToStage = useMoveToStage(partnerAPI)
return (
<button onClick={() => moveToStage.mutate({ customerId, stageId: newStageId })}>
Move to Next Stage
</button>
)
}Approve Customer
import { useApproveCustomer } from '@next-odoo/partner'
export function ApproveButton({ customerId }: { customerId: number }) {
const approveCustomer = useApproveCustomer(partnerAPI)
const handleApprove = async () => {
await approveCustomer.mutateAsync({
customerId,
notes: 'Approved after verification'
})
}
return <button onClick={handleApprove}>Approve</button>
}Reject Customer
import { useRejectCustomer } from '@next-odoo/partner'
export function RejectButton({ customerId }: { customerId: number }) {
const rejectCustomer = useRejectCustomer(partnerAPI)
const handleReject = async () => {
await rejectCustomer.mutateAsync({
customerId,
reason: 'Incomplete information'
})
}
return <button onClick={handleReject}>Reject</button>
}Search Customers
import { useSearchCustomers } from '@next-odoo/partner'
import { useState } from 'react'
export function CustomerSearch() {
const [query, setQuery] = useState('')
const { data: results } = useSearchCustomers(partnerAPI, query, 20)
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search customers..."
/>
{results?.map(customer => (
<div key={customer.id}>{customer.name}</div>
))}
</div>
)
}NOO (New Outlet Opening)
Get NOO Candidates
import { useNOOCandidates } from '@next-odoo/partner'
export function NOODashboard() {
const { data: candidates } = useNOOCandidates(partnerAPI)
return (
<div>
<h2>NOO Applications ({candidates?.length})</h2>
{/* Render NOO applications */}
</div>
)
}Create NOO Application
import { useCreateNOO } from '@next-odoo/partner'
export function CreateNOOForm({ partnerId }: { partnerId: number }) {
const createNOO = useCreateNOO(partnerAPI)
const handleSubmit = async () => {
await createNOO.mutateAsync({
partner_id: partnerId,
business_type: 'retail',
estimated_monthly_volume: 10000000,
investment_capacity: 50000000,
business_experience: '5 years in retail'
})
}
return <button onClick={handleSubmit}>Submit NOO Application</button>
}API Reference
PartnerAPI
Direct API methods (used by hooks):
const partnerAPI = createPartnerAPI(odooClient)
// Query methods
await partnerAPI.getCustomers(options)
await partnerAPI.getCustomerById(id)
await partnerAPI.getCanvasCustomers()
await partnerAPI.getTakingOrderCustomers()
await partnerAPI.getProspects()
await partnerAPI.getPendingApproval()
await partnerAPI.getCanvasStages()
await partnerAPI.searchCustomers(query, limit)
// Mutation methods
await partnerAPI.createCustomer(data)
await partnerAPI.updateCustomer(id, updates)
await partnerAPI.deleteCustomer(id)
await partnerAPI.convertToCanvas(customerId, stageId)
await partnerAPI.moveToStage(customerId, stageId)
await partnerAPI.approveCustomer(customerId, notes)
await partnerAPI.rejectCustomer(customerId, reason)
await partnerAPI.requestValidation(customerId)
// NOO methods
await partnerAPI.getNOOCandidates()
await partnerAPI.createNOOApplication(data)
await partnerAPI.approveNOO(nooId)
await partnerAPI.rejectNOO(nooId, reason)Types
Customer
interface Customer {
id: number
name: string
ref?: string
email?: string
phone?: string
mobile?: string
street?: string
city?: string
customer_type: 'canvas' | 'taking_order'
approval_status?: 'draft' | 'pending_approval' | 'approved' | 'rejected'
is_canvas_customer: boolean
canvas_stage_id?: number
latitude?: number
longitude?: number
// ... many more fields
}CanvasStage
interface CanvasStage {
id: number
name: string
sequence: number
is_initial_stage: boolean
is_final_stage: boolean
}CandidateNOO
interface CandidateNOO {
id: number
partner_id: number
business_type: 'retail' | 'wholesaler' | 'distributor' | 'other'
state: 'draft' | 'submitted' | 'under_review' | 'approved' | 'rejected' | 'converted'
// ... more fields
}Best Practices
- Reuse API instance - Create once, use across all hooks
- Use query keys - Leverage
partnerKeysfor cache invalidation - Handle loading states - Always check
isLoadinganderror - Optimistic updates - React Query handles this automatically
- Stale time - Configured per hook (2-30 minutes)
License
MIT
