@kodeme-io/next-core-product
v0.3.2
Published
Product catalog module for Next.js + Odoo applications
Maintainers
Readme
@next-odoo/product
Product catalog module for Next.js + Odoo applications with barcode search, stock tracking, and inventory management.
Features
- ✅ Product Management - CRUD operations for products
- ✅ Barcode Search - Find products by barcode
- ✅ Stock Tracking - Real-time inventory levels
- ✅ Category Support - Product categorization
- ✅ UOM Management - Multiple units of measure
- ✅ Variant Support - Product variants/templates
- ✅ React Query Hooks - Caching and optimistic updates
- ✅ Type Safety - Full TypeScript support
Installation
pnpm add @next-odoo/product @next-odoo/odoo-api @tanstack/react-queryUsage
Setup
import { createOdooClient } from '@next-odoo/odoo-api'
import { createProductAPI } from '@next-odoo/product'
const odoo = createOdooClient({
url: process.env.NEXT_PUBLIC_ODOO_URL!,
database: process.env.NEXT_PUBLIC_ODOO_DB!,
sessionId: userSessionId
})
const productAPI = createProductAPI(odoo)Query Hooks
Get All Products
import { useProducts } from '@next-odoo/product'
export function ProductList() {
const { data: products, isLoading } = useProducts(productAPI, {
domain: [['sale_ok', '=', true]],
limit: 100,
order: 'name ASC'
})
if (isLoading) return <div>Loading...</div>
return (
<ul>
{products?.map(product => (
<li key={product.id}>
{product.name} - ${product.list_price}
</li>
))}
</ul>
)
}Get Single Product
import { useProduct } from '@next-odoo/product'
export function ProductDetail({ id }: { id: number }) {
const { data: product } = useProduct(productAPI, id)
if (!product) return null
return (
<div>
<h1>{product.name}</h1>
<p>SKU: {product.default_code}</p>
<p>Price: ${product.list_price}</p>
<p>Stock: {product.qty_available} {product.uom_name}</p>
</div>
)
}Search by Barcode
import { useProductByBarcode } from '@next-odoo/product'
import { useState } from 'react'
export function BarcodeScanner() {
const [barcode, setBarcode] = useState('')
const { data: product, isLoading } = useProductByBarcode(productAPI, barcode)
return (
<div>
<input
value={barcode}
onChange={(e) => setBarcode(e.target.value)}
placeholder="Scan barcode..."
/>
{isLoading && <div>Searching...</div>}
{product && (
<div>
Found: {product.name} - ${product.list_price}
</div>
)}
</div>
)
}Search Products
import { useSearchProducts } from '@next-odoo/product'
export function ProductSearch() {
const [query, setQuery] = useState('')
const { data: results } = useSearchProducts(productAPI, query, 20)
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{results?.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}Get Products by Category
import { useProductsByCategory } from '@next-odoo/product'
export function CategoryProducts({ categoryId }: { categoryId: number }) {
const { data: products } = useProductsByCategory(productAPI, categoryId)
return (
<div>
<h2>Products in Category ({products?.length})</h2>
{/* Render products */}
</div>
)
}Get Product Categories
import { useProductCategories } from '@next-odoo/product'
export function CategoryTree() {
const { data: categories } = useProductCategories(productAPI)
return (
<ul>
{categories?.map(cat => (
<li key={cat.id}>{cat.complete_name}</li>
))}
</ul>
)
}Check Stock Levels
import { useProductStock } from '@next-odoo/product'
export function StockIndicator({ productId }: { productId: number }) {
const { data: stock } = useProductStock(productAPI, productId)
if (!stock) return null
return (
<div>
<p>Available: {stock.qty_available}</p>
<p>Forecasted: {stock.virtual_available}</p>
<p>Incoming: {stock.incoming_qty}</p>
<p>Outgoing: {stock.outgoing_qty}</p>
</div>
)
}Check Availability
import { useCheckAvailability } from '@next-odoo/product'
export function AvailabilityChecker({ productId, quantity }: Props) {
const { data } = useCheckAvailability(productAPI, productId, quantity)
return (
<div>
{data?.available ? (
<span className="text-green-600">✓ In Stock</span>
) : (
<span className="text-red-600">
✗ Only {data?.qty_available} available
</span>
)}
</div>
)
}Mutation Hooks
Create Product
import { useCreateProduct } from '@next-odoo/product'
export function CreateProductForm() {
const createProduct = useCreateProduct(productAPI)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
const productId = await createProduct.mutateAsync({
name: 'New Product',
list_price: 99.99,
categ_id: 1,
uom_id: 1,
active: true
})
console.log('Created product:', productId)
} catch (error) {
console.error('Failed to create product:', error)
}
}
return <form onSubmit={handleSubmit}>{/* Form fields */}</form>
}Update Product
import { useUpdateProduct } from '@next-odoo/product'
export function EditProduct({ productId }: { productId: number }) {
const updateProduct = useUpdateProduct(productAPI)
const handleUpdate = async () => {
await updateProduct.mutateAsync({
id: productId,
updates: {
list_price: 149.99,
description: 'Updated description'
}
})
}
return <button onClick={handleUpdate}>Update Price</button>
}Advanced Features
Product Variants
import { useProductTemplates, useProductVariants } from '@next-odoo/product'
export function ProductVariantSelector({ templateId }: { templateId: number }) {
const { data: variants } = useProductVariants(productAPI, templateId)
return (
<select>
{variants?.map(variant => (
<option key={variant.id} value={variant.id}>
{variant.name} - ${variant.list_price}
</option>
))}
</select>
)
}Units of Measure
import { useUnitOfMeasures } from '@next-odoo/product'
export function UOMSelector() {
const { data: uoms } = useUnitOfMeasures(productAPI)
return (
<select>
{uoms?.map(uom => (
<option key={uom.id} value={uom.id}>
{uom.name}
</option>
))}
</select>
)
}API Reference
ProductAPI
Direct API methods (used by hooks):
const productAPI = createProductAPI(odooClient)
// Query methods
await productAPI.getProducts(options)
await productAPI.getProductById(id)
await productAPI.getProductByBarcode(barcode)
await productAPI.searchProducts(query, limit)
await productAPI.getProductsByCategory(categoryId)
await productAPI.getCategories()
await productAPI.getUnitOfMeasures()
await productAPI.getProductStock(productId, locationId)
await productAPI.checkAvailability(productId, quantity)
// Mutation methods
await productAPI.createProduct(data)
await productAPI.updateProduct(id, updates)
await productAPI.deleteProduct(id) // Archives product
// Template/Variant methods
await productAPI.getProductTemplates(options)
await productAPI.getProductVariants(templateId)Types
Product
interface Product {
id: number
name: string
default_code?: string // SKU
barcode?: string
uom_id: number
uom_name: string
secondary_uom_id?: number
secondary_uom_name?: string
list_price: number // Sale price
standard_price: number // Cost price
categ_id: number
categ_name: string
active: boolean
type?: 'consu' | 'service' | 'product'
qty_available?: number // Stock on hand
virtual_available?: number // Forecasted stock
// ... more fields
}ProductCategory
interface ProductCategory {
id: number
name: string
parent_id?: [number, string]
complete_name?: string // Full path
}UnitOfMeasure
interface UnitOfMeasure {
id: number
name: string
category_id: [number, string]
factor: number
rounding: number
}Best Practices
- Reuse API instance - Create once, use across all hooks
- Leverage caching - React Query handles smart caching
- Real-time stock - Stock refetches every 5 minutes automatically
- Barcode scanning - Use
useProductByBarcodefor instant lookup - Category filtering - Use
child_ofdomain for hierarchical categories
License
MIT
