Files
medusa-store/www/apps/resources/app/recipes/erp/odoo/page.mdx

896 lines
34 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Prerequisites, WorkflowDiagram } from "docs-ui"
export const metadata = {
title: `Integrate Odoo with Medusa`,
}
# {metadata.title}
In this guide, you will learn how to implement the integration layer between Odoo and Medusa.
When you install a Medusa application, you get a fully-fledged commerce platform that supports customizations. However, your business might already be using other systems such as an ERP to centralize data and processes. The Medusa Framework facilitates integrating the ERP system and using its data to enrich your commerce platform.
Odoo is a suite of open-source business apps that covers all your business needs, including an ERP system. You can use Odoo to store products and their prices, manage orders, and more.
This guide will teach you how to implement the general integration between Medusa and Odoo. You will learn how to connect to Odoo's APIs and fetch data such as products. You can then expand on this integration to implement your business requirements. You can also refer to [this recipe](../page.mdx) to find general examples of ERP integration use cases and how to implement them.
---
## Step 1: Install a Medusa Application
<Prerequisites items={[
{
text: "Node.js v20+",
link: "https://nodejs.org/en/download"
},
{
text: "Git CLI tool",
link: "https://git-scm.com/downloads"
},
{
text: "PostgreSQL",
link: "https://www.postgresql.org/download/"
}
]} />
Start by installing the Medusa application on your machine with the following command:
```bash
npx create-medusa-app@latest
```
You will first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](../../../nextjs-starter/page.mdx).
Afterward, the installation process will start, installing the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.
<Note title="Why is the storefront installed separately">
The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](!docs!/learn/fundamentals/api-routes). Learn more about Medusa's architecture in [this documentation](!docs!/learn/introduction/architecture).
</Note>
Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.
<Note title="Ran into Errors">
Check out the [troubleshooting guides](../../../troubleshooting/create-medusa-app-errors/page.mdx) for help.
</Note>
---
## Step 2: Install JSONRPC Package
[Odoo's APIs](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html) are based on the XML-RPC and JSON-RPC protocols. So, to connect to Odoo's APIs, you need a JSON-RPC client library.
Run the following command in the Medusa application to install the `json-rpc-2.0` package:
```bash npm2yarn
npm install json-rpc-2.0
```
You will use this package in the next steps to connect to Odoo's APIs.
---
## Step 3: Create Odoo Module
To integrate third-party systems into Medusa, you create a custom [module](!docs!/learn/fundamentals/modules). A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.
In this step, you'll create an Odoo Module that provides the interface to connect to and interact with Odoo. You will later use this module when implementing the product syncing logic.
<Note>
Learn more about modules in [this documentation](!docs!/learn/fundamentals/modules).
</Note>
### Create Module Directory
A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/odoo`.
![Diagram showcasing directory structure after creating the module directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1740474975/Medusa%20Resources/odoo-1_en3bso.jpg)
### Create Service
You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.
Medusa registers the module's service in the [Medusa container](!docs!/learn/fundamentals/medusa-container), allowing you to easily resolve the service from other customizations and use its methods.
<Note title="What is the Medusa Container?">
The Medusa application registers resources, such as a module's service or the [logging tool](!docs!/learn/debugging-and-testing/logging), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](!docs!/learn/fundamentals/medusa-container).
</Note>
In this section, you'll create the Odoo Module's service and the methods necessary to connect to Odoo.
To create the service, create the file `src/modules/odoo/service.ts` with the following content:
![Diagram showcasing directory structure after creating the service file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740474976/Medusa%20Resources/odoo-2_y76dfs.jpg)
```ts title="src/modules/odoo/service.ts"
import { JSONRPCClient } from "json-rpc-2.0"
type Options = {
url: string
dbName: string
username: string
apiKey: string
}
export default class OdooModuleService {
private options: Options
private client: JSONRPCClient
constructor({}, options: Options) {
this.options = options
this.client = new JSONRPCClient((jsonRPCRequest) => {
fetch(`${options.url}/jsonrpc`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(jsonRPCRequest),
}).then((response) => {
if (response.status === 200) {
// Use client.receive when you received a JSON-RPC response.
return response
.json()
.then((jsonRPCResponse) => this.client.receive(jsonRPCResponse))
} else if (jsonRPCRequest.id !== undefined) {
return Promise.reject(new Error(response.statusText))
}
})
})
}
}
```
You create an `OdooModuleService` class that has two class properties:
1. `options`: An object that holds the Odoo Module's options. Those include the API key, URL, database name, and username. You'll learn how to pass those to the module later.
2. `client`: An instance of the `JSONRPCClient` class from the `json-rpc-2.0` package. You'll use this client to connect to Odoo's APIs.
The service's constructor accepts as a second parameter the module's options. So, you use those to initialize the `options` property and create the `client` property. The `client` property is initialized with a function that sends a JSON-RPC request to Odoo's API and receives the response.
Next, you will add the methods to log in and fetch data from Odoo.
### Login Method
Before sending any request to Odoo's APIs, you need to have an authenticated UID of the user. So, you'll implement a method to retrieve that UID when it's not set.
Start by adding a `uid` property to the `OdooModuleService` class:
```ts title="src/modules/odoo/service.ts"
export default class OdooModuleService {
private uid?: number
// ...
}
```
Then, add the following `login` method:
```ts title="src/modules/odoo/service.ts"
export default class OdooModuleService {
// ...
async login() {
this.uid = await this.client.request("call", {
service: "common",
method: "authenticate",
args: [
this.options.dbName,
this.options.username,
this.options.apiKey,
{},
],
})
}
}
```
The `login` method sends a JSON-RPC request to [Odoo's API to authenticate the user](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html#logging-in). It uses the `client` property to send a request with the `service`, `method`, and `args` properties.
If the authentication was successful, Odoo returns a UID, which you store in the `uid` property.
### Fetch Products Method
You can fetch many data from Odoo based on your business requirements, or create data in Odoo. For this guide, you'll only learn how to fetch products. You will use this method later to sync products from Odoo to Medusa.
First, add the following types to `src/modules/odoo/service.ts`:
```ts title="src/modules/odoo/service.ts"
export type Pagination = {
offset?: number
limit?: number
}
export type OdooProduct = {
id: number
display_name: string
is_published: boolean
website_url: string
name: string
list_price: number
description: string | false
description_sale: string | false
product_variant_ids: OdooProductVariant[]
qty_available: number
location_id: number | false
taxes_id: number[]
hs_code: string | false
allow_out_of_stock_order: boolean
is_kits: boolean
image_1920: string
image_1024: string
image_512: string
image_256: string
image_128: string
attribute_line_ids: {
attribute_id: {
display_name: string
}
value_ids: {
display_name: string
}[]
}[]
currency_id: {
id: number
display_name: string
}
}
export type OdooProductVariant = Omit<
OdooProduct,
"product_variant_ids" | "attribute_line_ids"
> & {
product_template_variant_value_ids: {
id: number
name: string
attribute_id: {
display_name: string
}
}[]
code: string
}
```
You define the following types:
- `Pagination`: An object that holds the pagination options for fetching products.
- `OdooProduct`: An object that represents an Odoo product. You define the properties that you'll fetch from Odoo's API. You can add more properties based on your business requirements.
- `OdooProductVariant`: An object that represents an Odoo product variant. You define the properties that you'll fetch from Odoo's API. You can add more properties based on your business requirements.
Then, add the following `listProducts` method to the `OdooModuleService` class:
```ts title="src/modules/odoo/service.ts"
export default class OdooModuleService {
// ...
async listProducts(filters?: any, pagination?: Pagination) {
if (!this.uid) {
await this.login()
}
const { offset, limit } = pagination || { offset: 0, limit: 10 }
const ids = await this.client.request("call", {
service: "object",
method: "execute_kw",
args: [
this.options.dbName,
this.uid,
this.options.apiKey,
"product.template",
"search",
filters || [[
["is_product_variant", "=", false],
]], {
offset,
limit,
},
],
})
// TODO retrieve product details based on ids
}
}
```
In the `listProducts` method, you first check if the user is authenticated, and call the `login` method otherwise. Then, you send a JSON-RPC request to retrieve product IDs from Odoo with pagination and filter options. Odoo's APIs require you to first retrieve the IDs of the products and then fetch the details of each product.
To retrieve the products, replace the `TODO` with the following:
```ts title="src/modules/odoo/service.ts"
// product fields to retrieve
const productSpecifications = {
id: {},
display_name: {},
is_published: {},
website_url: {},
name: {},
list_price: {},
description: {},
description_sale: {},
qty_available: {},
location_id: {},
taxes_id: {},
hs_code: {},
allow_out_of_stock_order: {},
is_kits: {},
image_1920: {},
image_1024: {},
image_512: {},
image_256: {},
currency_id: {
fields: {
display_name: {},
},
},
}
// retrieve products
const products: OdooProduct[] = await this.client.request("call", {
service: "object",
method: "execute_kw",
args: [
this.options.dbName,
this.uid,
this.options.apiKey,
type,
"web_read",
[ids],
{
specification: {
...productSpecifications,
product_variant_ids: {
fields: {
...productSpecifications,
product_template_variant_value_ids: {
fields: {
name: {},
attribute_id: {
fields: {
display_name: {},
},
},
},
context: {
show_attribute: false,
},
},
code: {},
},
context: {
show_code: false,
},
},
attribute_line_ids: {
fields: {
attribute_id: {
fields: {
display_name: {},
},
},
value_ids: {
fields: {
display_name: {},
},
context: {
show_attribute: false,
},
},
},
},
},
},
],
})
return products
```
You first define the `productSpecifications` object that holds the fields you want to fetch for each product and its variants. So, if you want to add more fields, you can add them in this object.
Then, you send a request to Odoo to fetch the products' details based on the IDs you retrieved earlier. You use the `productSpecifications` object to define the fields you want to fetch for each product and its variants. Finally, you return the fetched products.
You will use the `listProducts` method to sync products from Odoo to Medusa in the next steps.
### Export Module Definition
The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.
So, create the file `src/modules/odoo/index.ts` with the following content:
![Diagram showcasing directory structure after creating the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740475883/Medusa%20Resources/odoo-3_q5y803.jpg)
```ts title="src/modules/odoo/index.ts"
import OdooModuleService from "./service"
import { Module } from "@medusajs/framework/utils"
export const ODOO_MODULE = "odoo"
export default Module(ODOO_MODULE, {
service: OdooModuleService,
})
```
You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:
1. The module's name, which is `odoo`.
2. An object with a required property `service` indicating the module's service.
### Add Module to Medusa's Configurations
Once you finish building the module, add it to Medusa's configurations to start using it.
In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:
```ts title="medusa-config.ts"
module.exports = defineConfig({
// ...
modules: [
{
resolve: "./src/modules/odoo",
options: {
url: process.env.ODOO_URL,
dbName: process.env.ODOO_DB_NAME,
username: process.env.ODOO_USERNAME,
apiKey: process.env.ODOO_API_KEY,
},
},
],
})
```
Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` packages name. Modules that accept options also have an `options` property. You pass the options you defined in the `OdooModuleService` class to the module.
Then, set the environment variables in the `.env` file or system environment variables:
```plain
ODOO_URL=https://medusa8.odoo.com
ODOO_DB_NAME=medusa8
ODOO_USERNAME=test@gmail.com # username or email
ODOO_API_KEY=12345...
```
Where:
- `ODOO_URL`: The URL of your Odoo instance, which is of the format `https://<domain>.odoo.com`.
- `ODOO_DB_NAME`: The name of the database in your Odoo instance, which is the same as the domain in the URL.
- `ODOO_USERNAME`: The username or email of an Odoo user.
- `ODOO_API_KEY`: The API key of an Odoo user, or the user's password. To retrieve an API Key:
- On your Odoo dashboard, click on the user's avatar at the top right and choose "My Profile" from the dropdown.
![My profile dropdown in Odoo](https://res.cloudinary.com/dza7lstvk/image/upload/v1740476295/Medusa%20Resources/Screenshot_2025-02-25_at_11.36.23_AM_h9iind.png)
- On your profile's page, click the "Account Security" tab, then the "New API Key" button.
![Profile page in Odoo](https://res.cloudinary.com/dza7lstvk/image/upload/v1740476296/Medusa%20Resources/Screenshot_2025-02-25_at_11.36.55_AM_gl4wfu.png)
- In the pop-up that opens, enter your password.
- Enter the API Key's name, and set the expiration to "Persistent Key", then click the "Generate Key" button.
![Generate key pop-up](https://res.cloudinary.com/dza7lstvk/image/upload/v1740476443/Medusa%20Resources/Screenshot_2025-02-25_at_11.40.19_AM_ntbx1c.png)
- Copy the generated API Key and use it as the `ODOO_API_KEY` environment variable's value.
You will test that the Odoo Module works as expected in the next steps.
---
## Step 4: Sync Products from Odoo to Medusa
There are different use cases you can implement when integrating an ERP like Odoo. One of them is syncing products from the ERP to Medusa. This way, you can manage products in Odoo and have them reflected in your commerce platform.
To implement the syncing functionality, you need to create a [workflow](!docs!/learn/fundamentals/workflows). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function, but with additional features like defining rollback logic for each step, performing long actions asynchronously, and tracking the progress of the steps.
After defining the workflow, you can execute it in other customizations, such as periodically or when an event occurs.
In this section, you'll create a workflow that syncs products from Odoo to Medusa. Then, you'll execute that workflow once a day using a [scheduled job](!docs!/learn/fundamentals/scheduled-jobs). The workflow has the following steps:
<WorkflowDiagram
workflow={{
name: "get-products-from-erp",
steps: [
{
type: "step",
name: "getProductsFromErp",
description: "Fetch products from Odoo",
depth: 1,
},
{
type: "step",
name: "useQueryGraphStep",
description: "Get Medusa store configurations to use when creating the products.",
depth: 1,
link: "/references/helper-steps/useQueryGraphStep",
},
{
type: "step",
name: "useQueryGraphStep",
description: "Get Medusa sales channels to use when creating the products.",
depth: 1,
link: "/references/helper-steps/useQueryGraphStep",
},
{
type: "step",
name: "useQueryGraphStep",
description: "Get Medusa shipping profiles to use when creating the products.",
depth: 1,
link: "/references/helper-steps/useQueryGraphStep",
},
{
type: "step",
name: "createProductsWorkflow",
description: "Create new products in Medusa.",
depth: 1,
link: "/references/medusa-workflows/createProductsWorkflow",
},
{
type: "step",
name: "updateProductsWorkflow",
description: "Update existing products in Medusa.",
depth: 1,
link: "/references/medusa-workflows/updateProductsWorkflow",
}
]
}}
hideLegend
/>
The only step you'll need to implement is the `getProductsFromErp` step. The other steps are available through Medusa's `@medusajs/medusa/core-flows` package.
### getProductsFromErp
The first step of the workflow is to retrieve the products from the ERP. So, create the file `src/workflows/sync-from-erp.ts` with the following content:
![Diagram showcasing directory structure after creating the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740479240/Medusa%20Resources/odoo-4_jrh5mx.jpg)
```ts title="src/workflows/sync-from-erp.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
type Input = {
offset: number
limit: number
}
const getProductsFromErp = createStep(
"get-products-from-erp",
async (input: Input, { container }) => {
const odooModuleService = container.resolve("odoo")
const products = await odooModuleService.listProducts(undefined, input)
return new StepResponse(products)
}
)
```
You create a step using `createStep` from the Workflows SDK. It accepts two parameters:
1. The step's name, which is `get-products-from-erp`.
2. An async function that executes the step's logic. The function receives two parameters:
- The input data for the step, which are the pagination fields `offset` and `limit`.
- An object holding the workflow's context, including the [Medusa Container](!docs!learn/fundamentals/medusa-container) that allows you to resolve Framework and commerce tools.
In this step, you resolve the Odoo Module's service from the container and use its `listProducts` method to fetch products from Odoo. You pass the pagination options from the input data to the method.
A step must return an instance of `StepResponse` which accepts as a parameter the data to return, which is in this case the products.
### Create Workflow
You can now create the workflow that syncs the products from Odoo to Medusa.
In the same `src/workflows/sync-from-erp.ts` file, add the following imports:
```ts title="src/workflows/sync-from-erp.ts"
import {
createWorkflow, transform, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
createProductsWorkflow, updateProductsWorkflow, useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import {
CreateProductWorkflowInputDTO, UpdateProductWorkflowInputDTO,
} from "@medusajs/framework/types"
```
Then, add the workflow after the step:
```ts title="src/workflows/sync-from-erp.ts"
export const syncFromErpWorkflow = createWorkflow(
"sync-from-erp",
(input: Input) => {
const odooProducts = getProductsFromErp(input)
// @ts-ignore
const { data: stores } = useQueryGraphStep({
entity: "store",
fields: [
"default_sales_channel_id",
],
})
// @ts-ignore
const { data: shippingProfiles } = useQueryGraphStep({
entity: "shipping_profile",
fields: ["id"],
pagination: {
take: 1,
},
}).config({ name: "shipping-profile" })
const externalIdsFilters = transform({
odooProducts,
}, (data) => {
return data.odooProducts.map((product) => `${product.id}`)
})
// @ts-ignore
const { data: existingProducts } = useQueryGraphStep({
entity: "product",
fields: ["id", "external_id", "variants.*"],
filters: {
// @ts-ignore
external_id: externalIdsFilters,
},
}).config({ name: "existing-products" })
// TODO prepare products to create and update
}
)
```
You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.
It accepts as a second parameter a constructor function, which is the workflow's implementation. The function receives the pagination options as a parameter. In the workflow, you:
- Call the `getProductsFromErp` step to fetch products from Odoo.
- Use the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep) to fetch the Medusa store configurations, sales channels, and shipping profiles. You'll use this data when creating the products in a later step.
- The `useQueryGraphStep` uses [Query](!docs!/learn/fundamentals/module-links/query), which is a tool that retrieves data across modules.
- To figure out which products need to be updated, you retrieve products filtered by their `external_id` field, which you'll set to the Odoo product's ID when you create the products next.
- Notice that you use `transform` from the Workflows SDK to create the external IDs filters. That's because data manipulation is not allowed in a workflow. You can learn more about this and other restrictions in [this documentation](!docs!/learn/fundamentals/workflows/constructor-constraints).
Next, you need to prepare the products that should be created or updated. To do that, replace the `TODO` with the following:
```ts title="src/workflows/sync-from-erp.ts"
const {
productsToCreate,
productsToUpdate,
} = transform({
existingProducts,
odooProducts,
shippingProfiles,
stores,
}, (data) => {
const productsToCreate: CreateProductWorkflowInputDTO[] = []
const productsToUpdate: UpdateProductWorkflowInputDTO[] = []
data.odooProducts.forEach((odooProduct) => {
const product: CreateProductWorkflowInputDTO | UpdateProductWorkflowInputDTO = {
external_id: `${odooProduct.id}`,
title: odooProduct.display_name,
description: odooProduct.description || odooProduct.description_sale || "",
status: odooProduct.is_published ? "published" : "draft",
options: odooProduct.attribute_line_ids.length ? odooProduct.attribute_line_ids.map((attribute) => {
return {
title: attribute.attribute_id.display_name,
values: attribute.value_ids.map((value) => value.display_name),
}
}) : [
{
title: "Default",
values: ["Default"],
},
],
hs_code: odooProduct.hs_code || "",
handle: odooProduct.website_url.replace("/shop/", ""),
variants: [],
shipping_profile_id: data.shippingProfiles[0].id,
sales_channels: [
{
id: data.stores[0].default_sales_channel_id || "",
},
],
}
const existingProduct = data.existingProducts.find((p) => p.external_id === product.external_id)
if (existingProduct) {
product.id = existingProduct.id
}
if (odooProduct.product_variant_ids?.length) {
product.variants = odooProduct.product_variant_ids.map((variant) => {
const options = {}
if (variant.product_template_variant_value_ids.length) {
variant.product_template_variant_value_ids.forEach((value) => {
options[value.attribute_id.display_name] = value.name
})
} else {
product.options?.forEach((option) => {
options[option.title] = option.values[0]
})
}
return {
id: existingProduct ? existingProduct.variants.find((v) => v.sku === variant.code)?.id : undefined,
title: variant.display_name.replace(`[${variant.code}] `, ""),
sku: variant.code || undefined,
options,
prices: [
{
amount: variant.list_price,
currency_code: variant.currency_id.display_name.toLowerCase(),
},
],
manage_inventory: false, // change to true if syncing inventory from Odoo
metadata: {
external_id: `${variant.id}`,
},
}
})
} else {
product.variants?.push({
id: existingProduct ? existingProduct.variants[0].id : undefined,
title: "Default",
options: {
Default: "Default",
},
// @ts-ignore
prices: [
{
amount: odooProduct.list_price,
currency_code: odooProduct.currency_id.display_name.toLowerCase(),
},
],
metadata: {
external_id: `${odooProduct.id}`,
},
manage_inventory: false, // change to true if syncing inventory from Odoo
})
}
if (existingProduct) {
productsToUpdate.push(product as UpdateProductWorkflowInputDTO)
} else {
productsToCreate.push(product as CreateProductWorkflowInputDTO)
}
})
return {
productsToCreate,
productsToUpdate,
}
})
// TODO create and update the products
```
You use `transform` again to prepare the products to create and update. It receives two parameters:
- An object with the data you'll use in the transform function.
- The transform function, which receives the object from the first parameter, and returns the data that can be used in the rest of the workflow.
In the transform function, you:
- Create the `productsToCreate` and `productsToUpdate` arrays to hold the products that should be created and updated, respectively.
- Iterate over the products fetched from Odoo and create a product object for each. You set the product's properties based on the Odoo product's properties. If you want to add more properties, you can do so at this point.
- Most importantly, you set the `external_id` to the Odoo product's ID, which allows you later to identify the product later when updating it or for other operations.
- You also set the product's variants either to Odoo's variants or to a default variant. You set the product variant's Odoo ID in the `metadata.external_id` field, which allows you to identify the variant later when updating it or for other operations.
- To determine if a product already exists, you check if the product's `external_id` matches an existing product's `external_id`. You add it to the products to be updated. You apply a similar logic for the variants.
- Finally, you return an object with the `productsToCreate` and `productsToUpdate` arrays.
You can now create and update the products in the workflow. Replace the `TODO` with the following:
```ts title="src/workflows/sync-from-erp.ts"
createProductsWorkflow.runAsStep({
input: {
products: productsToCreate,
},
})
updateProductsWorkflow.runAsStep({
input: {
products: productsToUpdate,
},
})
return new WorkflowResponse({
odooProducts,
})
```
You use the `createProductsWorkflow` and `updateProductsWorkflow` to create and update the products returned from the transform function. Since both of these are workflows, you use the `runAsStep` method to run them as steps in the current workflow.
Finally, a workflow must return a instance of `WorkflowResponse` passing it as a parameter the data to return, which in this case is the products fetched from Odoo.
You can now execute this workflow in other customizations, such as a scheduled job.
### Create Scheduled Job
In Medusa, you can run a task at a specified interval using a [scheduled job](!docs!/learn/fundamentals/scheduled-jobs). A scheduled job is an asynchronous function that runs at a regular interval during the Medusa application's runtime to perform tasks such as syncing products from Odoo to Medusa.
To create a scheduled job, create the file `src/jobs/sync-products-from-erp.ts` with the following content:
![Diagram showcasing directory structure after creating the scheduled job file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740480934/Medusa%20Resources/odoo-5_xf0xug.jpg)
```ts title="src/jobs/sync-products-from-erp.ts"
import {
MedusaContainer,
} from "@medusajs/framework/types"
import { syncFromErpWorkflow } from "../workflows/sync-from-erp"
import { OdooProduct } from "../modules/odoo/service"
export default async function syncProductsJob(container: MedusaContainer) {
const limit = 10
let offset = 0
let total = 0
let odooProducts: OdooProduct[] = []
console.log("Syncing products...")
do {
odooProducts = (await syncFromErpWorkflow(container).run({
input: {
limit,
offset,
},
})).result.odooProducts
offset += limit
total += odooProducts.length
} while (odooProducts.length > 0)
console.log(`Synced ${total} products`)
}
export const config = {
name: "daily-product-sync",
schedule: "0 0 * * *", // Every day at midnight
}
```
In this file, you export:
- An asynchronous function, which is the task to execute at the specified schedule.
- A configuration object having the following properties:
- `name`: A unique name for the scheduled job.
- `schedule`: A [cron expression](https://crontab.guru/) string indicating the schedule to run the job at. The specified schedule indicates that this job should run every day at midnight.
The scheduled job function accepts the Medusa container as a parameter. In the function, you define the pagination options for the products to fetch from Odoo. You then run the `syncFromErpWorkflow` workflow with the pagination options. You increment the offset by the limit each time you run the workflow until you fetch all the products.
---
## Test it Out
To test out syncing the products from Odoo to Medusa, first, change the schedule of the job in `src/jobs/sync-products-from-erp.ts` to run every minute:
```ts title="src/jobs/sync-products-from-erp.ts"
export const config = {
name: "daily-product-sync",
schedule: "* * * * *", // Every minute
}
```
Then, start the Medusa application with the following command:
```bash npm2yarn
npm run dev
```
A minute later, you should find the message `Syncing products...` in the console. Once the job finishes, you should see the message `Synced <number> products`, indicating the number of products synced.
You can also confirm that the products were synced by checking the products in the Medusa Admin dashboard.
If you encounter any issues, make sure the module options are set correctly as explained in [this section](#add-module-to-medusas-configurations).
---
## Next Steps
You now have the foundation for integrating Odoo with Medusa. You can expand on this integration to implement more use cases, such as syncing orders, restricting purchases of products based on custom rules, and checking inventory in Odoo before adding to the cart. You can find the approach to implement these use cases in [this recipe](../page.mdx).
If you're new to Medusa, check out the [main documentation](!docs!/learn), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](../../../commerce-modules/page.mdx).