chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
import { z, IntegrationDefinitionProps } from '@botpress/sdk'
|
||||
|
||||
const odooContextSchema = z
|
||||
.record(z.string(), z.unknown())
|
||||
.title('Context')
|
||||
.describe('Optional Odoo context values to pass with the request.')
|
||||
|
||||
const odooRecordSchema = z
|
||||
.record(z.string(), z.unknown())
|
||||
.title('Values')
|
||||
.describe('Odoo field values keyed by field name.')
|
||||
|
||||
const odooDomainConditionSchema = z.array(z.unknown())
|
||||
|
||||
const odooDomainSchema = z
|
||||
.array(z.union([odooDomainConditionSchema, z.enum(['&', '|', '!'])]))
|
||||
.title('Domain')
|
||||
.describe('Odoo domain filters, such as [["id", "in", [1, 2, 3]]].')
|
||||
|
||||
const fieldsSchema = z
|
||||
.array(z.string())
|
||||
.title('Fields')
|
||||
.describe(
|
||||
'Odoo technical field names to include in the response. Only use fields known to exist on the target Odoo model.'
|
||||
)
|
||||
const contactFieldsSchema = fieldsSchema.describe(
|
||||
'Odoo res.partner field names to include in the response. Call listContactFields before choosing fields unless the exact field names were already retrieved in this conversation. Do not request CRM lead fields here; use searchLeads or listLeadFields for crm.lead fields.'
|
||||
)
|
||||
const leadFieldsSchema = fieldsSchema.describe(
|
||||
'Odoo crm.lead field names to include in the response. Call listLeadFields before choosing fields unless the exact field names were already retrieved in this conversation. To find which contacts are also leads, first retrieve the available contact and lead fields, then read comparable fields.'
|
||||
)
|
||||
const ticketFieldsSchema = fieldsSchema.describe(
|
||||
'Odoo helpdesk.ticket field names to include in the response. Call listTicketFields before choosing fields unless the exact field names were already retrieved in this conversation.'
|
||||
)
|
||||
const contactIdsSchema = z.array(z.number()).title('Contact IDs').describe('Odoo contact record IDs.')
|
||||
const leadIdsSchema = z.array(z.number()).title('Lead IDs').describe('Odoo CRM lead record IDs.')
|
||||
const ticketIdsSchema = z.array(z.number()).title('Ticket IDs').describe('Odoo helpdesk ticket record IDs.')
|
||||
|
||||
const contactValuesSchema = z
|
||||
.object({
|
||||
name: z.string().title('Name').describe('Contact or company display name.'),
|
||||
email: z.string().title('Email').describe('Contact email address.').optional(),
|
||||
phone: z.string().title('Phone').describe('Contact phone number.').optional(),
|
||||
parent_id: z.number().title('Company ID').describe('Related company/contact ID for this contact.').optional(),
|
||||
is_company: z.boolean().title('Is Company').describe('Whether this contact represents a company.').optional(),
|
||||
street: z.string().title('Street').describe('Street address.').optional(),
|
||||
city: z.string().title('City').describe('City.').optional(),
|
||||
zip: z.string().title('ZIP').describe('Postal or ZIP code.').optional(),
|
||||
country_id: z.number().title('Country ID').describe('Related Odoo country ID.').optional(),
|
||||
state_id: z.number().title('State ID').describe('Related Odoo state ID.').optional(),
|
||||
vat: z.string().title('Tax ID').describe('Tax identification number.').optional(),
|
||||
website: z.string().title('Website').describe('Contact or company website.').optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.title('Contact Values')
|
||||
.describe(
|
||||
'Odoo contact field values keyed by field name. Use listContactFields before using custom fields; company names are usually represented by name or parent_id, not company_name.'
|
||||
)
|
||||
|
||||
const contactUpdateValuesSchema = contactValuesSchema
|
||||
.partial()
|
||||
.title('Contact Values')
|
||||
.describe('Odoo contact field values to update, keyed by field name.')
|
||||
|
||||
const leadValuesSchema = z
|
||||
.object({
|
||||
name: z.string().title('Name').describe('Lead or opportunity title.'),
|
||||
email_from: z.string().title('Email').describe('Lead email address.').optional(),
|
||||
phone: z.string().title('Phone').describe('Lead phone number.').optional(),
|
||||
mobile: z.string().title('Mobile').describe('Lead mobile phone number.').optional(),
|
||||
contact_name: z.string().title('Contact Name').describe('Name of the person associated with the lead.').optional(),
|
||||
partner_name: z.string().title('Company Name').describe('Company name associated with the lead.').optional(),
|
||||
partner_id: z.number().title('Customer ID').describe('Related Odoo customer/contact ID.').optional(),
|
||||
stage_id: z.number().title('Stage ID').describe('Odoo CRM stage ID.').optional(),
|
||||
user_id: z.number().title('Salesperson ID').describe('Assigned Odoo user ID.').optional(),
|
||||
team_id: z.number().title('Sales Team ID').describe('Assigned Odoo sales team ID.').optional(),
|
||||
type: z
|
||||
.enum(['lead', 'opportunity'])
|
||||
.title('Type')
|
||||
.describe('Whether the CRM record is a lead or opportunity.')
|
||||
.optional(),
|
||||
probability: z.number().title('Probability').describe('Success probability percentage.').optional(),
|
||||
expected_revenue: z.number().title('Expected Revenue').describe('Expected revenue for the opportunity.').optional(),
|
||||
description: z.string().title('Description').describe('Internal notes or description for the lead.').optional(),
|
||||
referred: z.string().title('Referred By').describe('Referral source text.').optional(),
|
||||
source_id: z.number().title('Source ID').describe('Odoo UTM source ID.').optional(),
|
||||
medium_id: z.number().title('Medium ID').describe('Odoo UTM medium ID.').optional(),
|
||||
campaign_id: z.number().title('Campaign ID').describe('Odoo UTM campaign ID.').optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.title('Lead Values')
|
||||
.describe('Odoo CRM lead field values keyed by field name.')
|
||||
|
||||
const leadUpdateValuesSchema = leadValuesSchema
|
||||
.partial()
|
||||
.title('Lead Values')
|
||||
.describe('Odoo CRM lead field values to update, keyed by field name.')
|
||||
|
||||
const ticketValuesSchema = z
|
||||
.object({
|
||||
name: z.string().title('Name').describe('Helpdesk ticket title.'),
|
||||
description: z.string().title('Description').describe('Ticket description or customer request details.').optional(),
|
||||
partner_id: z.number().title('Customer ID').describe('Related Odoo customer/contact ID.').optional(),
|
||||
partner_name: z.string().title('Customer Name').describe('Customer name for the ticket.').optional(),
|
||||
partner_email: z.string().title('Customer Email').describe('Customer email address for the ticket.').optional(),
|
||||
email_cc: z.string().title('Email CC').describe('Additional email recipients copied on the ticket.').optional(),
|
||||
team_id: z.number().title('Helpdesk Team ID').describe('Assigned Odoo helpdesk team ID.').optional(),
|
||||
user_id: z.number().title('Assigned User ID').describe('Assigned Odoo user ID.').optional(),
|
||||
stage_id: z.number().title('Stage ID').describe('Odoo helpdesk ticket stage ID.').optional(),
|
||||
ticket_type_id: z.number().title('Ticket Type ID').describe('Odoo helpdesk ticket type ID.').optional(),
|
||||
priority: z
|
||||
.enum(['0', '1', '2', '3'])
|
||||
.title('Priority')
|
||||
.describe('Odoo ticket priority, from 0 (lowest) to 3 (highest).')
|
||||
.optional(),
|
||||
tag_ids: z.array(z.number()).title('Tag IDs').describe('Odoo helpdesk ticket tag IDs.').optional(),
|
||||
company_id: z.number().title('Company ID').describe('Related Odoo company ID.').optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.title('Ticket Values')
|
||||
.describe('Odoo helpdesk ticket field values keyed by field name.')
|
||||
|
||||
const ticketUpdateValuesSchema = ticketValuesSchema
|
||||
.partial()
|
||||
.title('Ticket Values')
|
||||
.describe('Odoo helpdesk ticket field values to update, keyed by field name.')
|
||||
|
||||
const notDeletedContactSchema = z
|
||||
.object({
|
||||
id: z.number().title('Contact ID').describe('Odoo contact record ID.'),
|
||||
name: z.string().title('Contact Name').describe('Odoo contact display name.').optional(),
|
||||
reason: z.string().title('Reason').describe('Why the contact could not be deleted.'),
|
||||
})
|
||||
.title('Not Deleted Contact')
|
||||
.describe('An Odoo contact that could not be deleted.')
|
||||
|
||||
const notDeletedLeadSchema = z
|
||||
.object({
|
||||
id: z.number().title('Lead ID').describe('Odoo CRM lead record ID.'),
|
||||
name: z.string().title('Lead Name').describe('Odoo CRM lead display name.').optional(),
|
||||
reason: z.string().title('Reason').describe('Why the lead could not be deleted.'),
|
||||
})
|
||||
.title('Not Deleted Lead')
|
||||
.describe('An Odoo CRM lead that could not be deleted.')
|
||||
|
||||
export const actions = {
|
||||
getCurrentUser: {
|
||||
title: 'Get Current User',
|
||||
description: 'Get the Odoo user ID associated with the configured API key.',
|
||||
input: {
|
||||
schema: z.object({}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
id: z.number().title('User ID').describe('Odoo user ID associated with the configured API key.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
listContactFields: {
|
||||
title: 'List Odoo Contact Fields',
|
||||
description:
|
||||
'List available fields for Odoo contacts. Call this before searchContacts, listContacts, createContact, or updateContacts when selecting Odoo contact field names.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
allfields: contactFieldsSchema.optional(),
|
||||
attributes: z
|
||||
.array(z.string())
|
||||
.title('Attributes')
|
||||
.describe('Field metadata attributes to return, such as string, type, and required.')
|
||||
.optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
fields: z.record(z.string(), z.unknown()).title('Fields').describe('Field metadata keyed by Odoo field name.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
listLeadFields: {
|
||||
title: 'List Odoo Lead Fields',
|
||||
description:
|
||||
'List available fields for Odoo CRM leads. Call this before searchLeads, listLeads, createLead, or updateLeads when selecting Odoo lead field names.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
allfields: leadFieldsSchema.optional(),
|
||||
attributes: z
|
||||
.array(z.string())
|
||||
.title('Attributes')
|
||||
.describe('Field metadata attributes to return, such as string, type, and required.')
|
||||
.optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
fields: z
|
||||
.record(z.string(), z.unknown())
|
||||
.title('Fields')
|
||||
.describe('Field metadata keyed by Odoo lead field name.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
listTicketFields: {
|
||||
title: 'List Odoo Ticket Fields',
|
||||
description: 'List available fields for Odoo helpdesk tickets.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
allfields: ticketFieldsSchema.optional(),
|
||||
attributes: z
|
||||
.array(z.string())
|
||||
.title('Attributes')
|
||||
.describe('Field metadata attributes to return, such as string, type, and required.')
|
||||
.optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
fields: z
|
||||
.record(z.string(), z.unknown())
|
||||
.title('Fields')
|
||||
.describe('Field metadata keyed by Odoo ticket field name.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
searchContacts: {
|
||||
title: 'Search Contacts',
|
||||
description:
|
||||
'Search Odoo contacts using an Odoo domain and optional read parameters. Call listContactFields first unless the needed res.partner field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
domain: odooDomainSchema.optional(),
|
||||
fields: contactFieldsSchema.optional(),
|
||||
offset: z.number().title('Offset').describe('Number of matching contacts to skip.').optional(),
|
||||
limit: z.number().title('Limit').describe('Maximum number of contacts to return.').optional(),
|
||||
order: z.string().title('Order').describe('Odoo order expression, such as "name asc".').optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
records: z.array(odooRecordSchema).title('Records').describe('Matching Odoo contact records.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
searchLeads: {
|
||||
title: 'Search Leads',
|
||||
description:
|
||||
'Search Odoo CRM leads using an Odoo domain and optional read parameters. Call listLeadFields first unless the needed crm.lead field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
domain: odooDomainSchema.optional(),
|
||||
fields: leadFieldsSchema.optional(),
|
||||
offset: z.number().title('Offset').describe('Number of matching leads to skip.').optional(),
|
||||
limit: z.number().title('Limit').describe('Maximum number of leads to return.').optional(),
|
||||
order: z.string().title('Order').describe('Odoo order expression, such as "name asc".').optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
records: z.array(odooRecordSchema).title('Records').describe('Matching Odoo CRM lead records.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
listLeads: {
|
||||
title: 'List Leads',
|
||||
description:
|
||||
'Read Odoo CRM leads by ID. Call listLeadFields first unless the needed crm.lead field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: leadIdsSchema,
|
||||
fields: leadFieldsSchema.optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
records: z.array(odooRecordSchema).title('Records').describe('Requested Odoo CRM lead records.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
searchTickets: {
|
||||
title: 'Search Tickets',
|
||||
description:
|
||||
'Search Odoo helpdesk tickets using an Odoo domain and optional read parameters. Call listTicketFields first unless the needed helpdesk.ticket field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
domain: odooDomainSchema.optional(),
|
||||
fields: ticketFieldsSchema.optional(),
|
||||
offset: z.number().title('Offset').describe('Number of matching tickets to skip.').optional(),
|
||||
limit: z.number().title('Limit').describe('Maximum number of tickets to return.').optional(),
|
||||
order: z.string().title('Order').describe('Odoo order expression, such as "name asc".').optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
records: z.array(odooRecordSchema).title('Records').describe('Matching Odoo helpdesk ticket records.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
listTickets: {
|
||||
title: 'List Tickets',
|
||||
description:
|
||||
'List Odoo helpdesk tickets by ID. Call listTicketFields first unless the needed helpdesk.ticket field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: ticketIdsSchema,
|
||||
fields: ticketFieldsSchema.optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
records: z.array(odooRecordSchema).title('Records').describe('Requested Odoo helpdesk ticket records.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createLead: {
|
||||
title: 'Create Lead',
|
||||
description:
|
||||
'Create an Odoo CRM lead or opportunity. Call listLeadFields first unless the needed crm.lead field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
values: leadValuesSchema,
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
id: z.number().title('Lead ID').describe('ID of the created Odoo CRM lead.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createTicket: {
|
||||
title: 'Create Ticket',
|
||||
description:
|
||||
'Create an Odoo helpdesk ticket. Call listTicketFields first unless the needed helpdesk.ticket field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
values: ticketValuesSchema,
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
id: z.number().title('Ticket ID').describe('ID of the created Odoo helpdesk ticket.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
updateLeads: {
|
||||
title: 'Update Leads',
|
||||
description:
|
||||
'Update one or more Odoo CRM leads. Call listLeadFields first unless the needed crm.lead field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: leadIdsSchema,
|
||||
values: leadUpdateValuesSchema,
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
updatedIds: leadIdsSchema.describe('Odoo CRM lead record IDs that were updated.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
updateTickets: {
|
||||
title: 'Update Tickets',
|
||||
description:
|
||||
'Update one or more Odoo helpdesk tickets. Call listTicketFields first unless the needed helpdesk.ticket field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: ticketIdsSchema,
|
||||
values: ticketUpdateValuesSchema,
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
updatedIds: ticketIdsSchema.describe('Odoo helpdesk ticket record IDs that were updated.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
deleteLeads: {
|
||||
title: 'Delete Leads',
|
||||
description: 'Delete one or more Odoo CRM leads owned by the specified Odoo user.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: leadIdsSchema,
|
||||
ownerId: z
|
||||
.number()
|
||||
.title('Owner User ID')
|
||||
.describe('Odoo user ID that must match each lead owner before the lead is deleted.'),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
deletedIds: leadIdsSchema.describe('Odoo CRM lead record IDs that were deleted.'),
|
||||
notDeletedLeads: z
|
||||
.array(notDeletedLeadSchema)
|
||||
.title('Not Deleted Leads')
|
||||
.describe('Odoo CRM leads that could not be deleted, with the reason for each lead.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
deleteTickets: {
|
||||
title: 'Delete Tickets',
|
||||
description: 'Delete one or more Odoo helpdesk tickets.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: ticketIdsSchema,
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
deletedIds: ticketIdsSchema.describe('Odoo helpdesk ticket record IDs that were deleted.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
listContacts: {
|
||||
title: 'List Contacts',
|
||||
description:
|
||||
'Read Odoo contacts by ID. Call listContactFields first unless the needed res.partner field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: contactIdsSchema,
|
||||
fields: contactFieldsSchema.optional(),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
records: z.array(odooRecordSchema).title('Records').describe('Requested Odoo contact records.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createContact: {
|
||||
title: 'Create Contact',
|
||||
description:
|
||||
'Create an Odoo contact. Call listContactFields first unless the needed res.partner field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
values: contactValuesSchema,
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
id: z.number().title('Contact ID').describe('ID of the created Odoo contact.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
updateContacts: {
|
||||
title: 'Update Contacts',
|
||||
description:
|
||||
'Update one or more Odoo contacts. Call listContactFields first unless the needed res.partner field names were already retrieved in this conversation.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: contactIdsSchema,
|
||||
values: contactUpdateValuesSchema,
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
updatedIds: contactIdsSchema.describe('Odoo contact record IDs that were updated.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
deleteContacts: {
|
||||
title: 'Delete Contacts',
|
||||
description: 'Delete one or more Odoo contacts owned by the specified Odoo user.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
ids: contactIdsSchema,
|
||||
ownerId: z
|
||||
.number()
|
||||
.title('Owner User ID')
|
||||
.describe('Odoo user ID that must match each contact owner before the contact is deleted.'),
|
||||
context: odooContextSchema.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
deletedIds: contactIdsSchema.describe('Odoo contact record IDs that were deleted.'),
|
||||
notDeletedContacts: z
|
||||
.array(notDeletedContactSchema)
|
||||
.title('Not Deleted Contacts')
|
||||
.describe('Odoo contacts that could not be deleted, with the reason for each contact.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as const satisfies IntegrationDefinitionProps['actions']
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IntegrationDefinitionProps, z } from '@botpress/sdk'
|
||||
|
||||
export const configuration = {
|
||||
schema: z.object({
|
||||
url: z.string().url().title('Odoo URL').describe('Base URL of the Odoo instance.'),
|
||||
database: z.string().min(1).title('Database').describe('Odoo database name.'),
|
||||
apiKey: z.string().secret().min(1).title('API Key').describe('Odoo API key used to authenticate requests.'),
|
||||
}),
|
||||
} as const satisfies IntegrationDefinitionProps['configuration']
|
||||
@@ -0,0 +1,3 @@
|
||||
export { actions } from './actions'
|
||||
export { configuration } from './configuration'
|
||||
export { states } from './states'
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IntegrationDefinitionProps, z } from '@botpress/sdk'
|
||||
|
||||
export const states = {
|
||||
account: {
|
||||
type: 'integration',
|
||||
schema: z.object({
|
||||
userId: z.number().title('User ID').describe('Odoo user ID associated with the configured API key.'),
|
||||
}),
|
||||
},
|
||||
} as const satisfies IntegrationDefinitionProps['states']
|
||||
@@ -0,0 +1,52 @@
|
||||
The Odoo integration lets your Botpress chatbot work with Odoo contact and CRM lead records through Odoo's JSON-2 API. You can search, read, create, update, and delete contacts and leads, retrieve field metadata, and validate the configured Odoo API key against the current Odoo user.
|
||||
|
||||
## Configuration
|
||||
|
||||
Before installing the integration, make sure you have:
|
||||
|
||||
- An Odoo instance with JSON-2 API access enabled.
|
||||
- The Odoo database name.
|
||||
- An API key for an Odoo user that has access to the contact records your bot needs to manage.
|
||||
|
||||
In Botpress, configure the integration with the following fields:
|
||||
|
||||
| Field | Description |
|
||||
| -------- | ----------------------------------------------------------------------- |
|
||||
| Odoo URL | The base URL of your Odoo instance, such as `https://example.odoo.com`. |
|
||||
| Database | The Odoo database name. |
|
||||
| API Key | The Odoo API key used to authenticate JSON-2 API requests. |
|
||||
|
||||
When the integration is saved, Botpress validates the configuration by calling Odoo and retrieving the user ID associated with the API key. That user ID is stored in the integration state and can be used by actions that need to know which Odoo user is configured.
|
||||
|
||||
## Usage Notes
|
||||
|
||||
Odoo field values are passed as objects keyed by Odoo field name. For example, creating a contact might use values such as:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Ada Lovelace",
|
||||
"email": "ada@example.com",
|
||||
"phone": "+1 555 0100"
|
||||
}
|
||||
```
|
||||
|
||||
Odoo domains use the standard Odoo domain format. For example:
|
||||
|
||||
```json
|
||||
[["email", "=", "ada@example.com"]]
|
||||
```
|
||||
|
||||
You can pass an optional Odoo context object to contact and lead actions when you need to control Odoo-specific request behavior.
|
||||
|
||||
## Limitations
|
||||
|
||||
- This version focuses on Odoo contacts (`res.partner`) and CRM leads (`crm.lead`).
|
||||
- The integration does not currently handle Odoo webhooks or incoming events.
|
||||
- The integration does not provide Botpress channels for Odoo conversations.
|
||||
- Delete protection depends on the record's `user_id` owner field.
|
||||
- Available fields, permissions, and validation rules depend on your Odoo instance and the permissions of the configured API key.
|
||||
|
||||
## Changelog
|
||||
|
||||
- 0.2.0: Added Odoo CRM lead actions for field lookup, search, read, create, update, and delete.
|
||||
- 0.1.0: Added Odoo configuration validation and contact actions for field lookup, search, read, create, update, and delete.
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 919 495"><path d="M695,346a75,75,0,1,1,75-75A75,75,0,0,1,695,346Zm0-31a44,44,0,1,0-44-44A44,44,0,0,0,695,315ZM538,346a75,75,0,1,1,75-75A75,75,0,0,1,538,346Zm0-31a44,44,0,1,0-44-44A44,44,0,0,0,538,315Zm-82-45c0,41.9-33.6,76-75,76s-75-34-75-75.9S336.5,196,381,196c16.4,0,31.6,3.5,44,12.6V165.1c0-8.3,7.3-15.1,15.5-15.1s15.5,6.8,15.5,15.1Zm-75,45a44,44,0,1,0-44-44A44,44,0,0,0,381,315Z" style="fill:#8f8f8f"/><path d="M224,346a75,75,0,1,1,75-75A75,75,0,0,1,224,346Zm0-31a44,44,0,1,0-44-44A44,44,0,0,0,224,315Z" style="fill:#714b67"/></svg>
|
||||
|
After Width: | Height: | Size: 590 B |
@@ -0,0 +1,18 @@
|
||||
import { IntegrationDefinition } from '@botpress/sdk'
|
||||
import { actions, configuration, states } from './definitions'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'odoo',
|
||||
title: 'Odoo',
|
||||
description: 'Connect Botpress to Odoo records such as leads, contacts, and tickets.',
|
||||
version: '2.0.0',
|
||||
readme: 'hub.md',
|
||||
icon: 'icon.svg',
|
||||
attributes: {
|
||||
category: 'CRM & Sales',
|
||||
repo: 'botpress',
|
||||
},
|
||||
configuration,
|
||||
states,
|
||||
actions,
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@botpresshub/odoo",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit",
|
||||
"build": "bp add -y && bp build",
|
||||
"check:bplint": "bp lint",
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/common": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"@botpress/sdk-addons": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { createOdooRuntimeError } from './odoo-client/actions/errors'
|
||||
import actions from './odoo-client/actions/implementations'
|
||||
import { OdooClient } from './odoo-client/OdooClient'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default new bp.Integration({
|
||||
register: async (props) => {
|
||||
const { url, apiKey, database } = props.ctx.configuration
|
||||
const logger = props.logger.forBot()
|
||||
|
||||
logger.info(`Validating Odoo configuration for url=${url}, database=${database}`)
|
||||
|
||||
try {
|
||||
const odooClient = new OdooClient(url, apiKey, database)
|
||||
const userId = await odooClient.getCurrentUserId()
|
||||
|
||||
await props.client.setState({
|
||||
type: 'integration',
|
||||
id: props.ctx.integrationId,
|
||||
name: 'account',
|
||||
payload: { userId },
|
||||
})
|
||||
|
||||
logger.info(`Odoo configuration validated for user id ${userId}`)
|
||||
} catch (thrown) {
|
||||
logger.warn('Odoo configuration validation failed', {
|
||||
error: thrown instanceof Error ? thrown.message : String(thrown),
|
||||
})
|
||||
throw new sdk.RuntimeError(`Invalid Odoo configuration: ${createOdooRuntimeError(thrown).message}`)
|
||||
}
|
||||
},
|
||||
unregister: async () => {},
|
||||
actions,
|
||||
channels: {},
|
||||
handler: async () => {},
|
||||
})
|
||||
@@ -0,0 +1,234 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import type {
|
||||
GetFieldsOutput,
|
||||
GetFieldsRequest,
|
||||
Model,
|
||||
ResPartnerCreateInput,
|
||||
ResPartnerCreateOutput,
|
||||
ResPartnerReadInput,
|
||||
ResPartnerReadOutput,
|
||||
ResPartnerSearchReadInput,
|
||||
ResPartnerSearchReadOutput,
|
||||
ResPartnerUnlinkInput,
|
||||
ResPartnerUnlinkOutput,
|
||||
ResPartnerWriteInput,
|
||||
ResPartnerWriteOutput,
|
||||
ResUsersContextGetOutput,
|
||||
CrmLeadCreateInput,
|
||||
CrmLeadCreateOutput,
|
||||
CrmLeadFieldsGetInput,
|
||||
CrmLeadFieldsGetOutput,
|
||||
CrmLeadReadInput,
|
||||
CrmLeadReadOutput,
|
||||
CrmLeadSearchReadInput,
|
||||
CrmLeadSearchReadOutput,
|
||||
CrmLeadUnlinkInput,
|
||||
CrmLeadUnlinkOutput,
|
||||
CrmLeadWriteInput,
|
||||
CrmLeadWriteOutput,
|
||||
HelpdeskTicketCreateInput,
|
||||
HelpdeskTicketCreateOutput,
|
||||
HelpdeskTicketFieldsGetInput,
|
||||
HelpdeskTicketFieldsGetOutput,
|
||||
HelpdeskTicketReadInput,
|
||||
HelpdeskTicketReadOutput,
|
||||
HelpdeskTicketSearchReadInput,
|
||||
HelpdeskTicketSearchReadOutput,
|
||||
HelpdeskTicketUnlinkInput,
|
||||
HelpdeskTicketUnlinkOutput,
|
||||
HelpdeskTicketWriteInput,
|
||||
HelpdeskTicketWriteOutput,
|
||||
} from './types'
|
||||
|
||||
const modelMap: Record<Model, string> = {
|
||||
Lead: 'crm.lead',
|
||||
Contact: 'res.partner',
|
||||
Ticket: 'helpdesk.ticket',
|
||||
}
|
||||
const recordSchema = z.record(z.string(), z.unknown()).and(z.object({ id: z.number().optional() }))
|
||||
|
||||
const odooRecordArraySchema = z.array(recordSchema)
|
||||
|
||||
const helpdeskTicketRecordSchema = recordSchema.and(z.object({ id: z.number() }))
|
||||
const helpdeskTicketRecordArraySchema = z.array(helpdeskTicketRecordSchema)
|
||||
|
||||
const recordMapSchema = z.record(z.string(), recordSchema)
|
||||
|
||||
const createdIdSchema = z.tuple([z.number()])
|
||||
|
||||
const booleanSchema = z.boolean()
|
||||
const resUsersContextGetOutputSchema = recordSchema.and(z.object({ uid: z.number() }))
|
||||
|
||||
const odooErrorSchema = recordSchema.and(z.object({ name: z.string().optional(), message: z.string().optional() }))
|
||||
|
||||
const readOdooError = (errorMessage: string): string => {
|
||||
try {
|
||||
const errorBody = JSON.parse(errorMessage) as unknown
|
||||
const result = odooErrorSchema.safeParse(errorBody)
|
||||
|
||||
if (!result.success) {
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
const { name, message } = result.data
|
||||
|
||||
if (name && message) {
|
||||
return `${name}: ${message}`
|
||||
}
|
||||
|
||||
return message ?? name ?? errorMessage
|
||||
} catch {
|
||||
return errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
export class OdooClient {
|
||||
private _url: string
|
||||
private _apiKey: string
|
||||
private _database: string
|
||||
|
||||
public constructor(url: string, apiKey: string, database: string) {
|
||||
this._url = url.replace(/\/$/, '')
|
||||
this._apiKey = apiKey
|
||||
this._database = database
|
||||
}
|
||||
|
||||
private _getHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this._apiKey}`,
|
||||
'X-ODOO-DATABASE': this._database,
|
||||
}
|
||||
}
|
||||
|
||||
private async _postJson<TResponse>(
|
||||
endpoint: string,
|
||||
body: object,
|
||||
responseSchema: z.ZodType<TResponse>
|
||||
): Promise<TResponse> {
|
||||
const headers = this._getHeaders()
|
||||
|
||||
const response = await fetch(`${this._url}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await response.text()
|
||||
const details = errorMessage ? ` - ${readOdooError(errorMessage)}` : ''
|
||||
|
||||
throw new Error(`Odoo API request failed with status ${response.status} ${response.statusText}${details}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as unknown
|
||||
const parse = responseSchema.safeParse(data)
|
||||
if (!parse.success) {
|
||||
throw new Error(`Odoo API request failed: expected ${parse.error.message} response`)
|
||||
}
|
||||
|
||||
return parse.data
|
||||
}
|
||||
|
||||
public async listFields(model: Model, request: GetFieldsRequest): Promise<GetFieldsOutput> {
|
||||
return this._postJson(`/json/2/${modelMap[model]}/fields_get`, request, recordSchema)
|
||||
}
|
||||
|
||||
public async listLeadFields(input: CrmLeadFieldsGetInput): Promise<CrmLeadFieldsGetOutput> {
|
||||
return this._postJson('/json/2/crm.lead/fields_get', input, recordMapSchema)
|
||||
}
|
||||
|
||||
public async listTicketFields(input: HelpdeskTicketFieldsGetInput): Promise<HelpdeskTicketFieldsGetOutput> {
|
||||
return this._postJson('/json/2/helpdesk.ticket/fields_get', input, recordMapSchema)
|
||||
}
|
||||
|
||||
public async getCurrentUserId(): Promise<number> {
|
||||
const context = await this._postJson<ResUsersContextGetOutput>(
|
||||
'/json/2/res.users/context_get',
|
||||
{},
|
||||
resUsersContextGetOutputSchema
|
||||
)
|
||||
|
||||
return context.uid
|
||||
}
|
||||
|
||||
public async searchContacts(input: ResPartnerSearchReadInput): Promise<ResPartnerSearchReadOutput> {
|
||||
return this._postJson('/json/2/res.partner/search_read', input, odooRecordArraySchema)
|
||||
}
|
||||
|
||||
public async searchLeads(input: CrmLeadSearchReadInput): Promise<CrmLeadSearchReadOutput> {
|
||||
return this._postJson('/json/2/crm.lead/search_read', input, odooRecordArraySchema)
|
||||
}
|
||||
|
||||
public async searchTickets(input: HelpdeskTicketSearchReadInput): Promise<HelpdeskTicketSearchReadOutput> {
|
||||
return this._postJson('/json/2/helpdesk.ticket/search_read', input, helpdeskTicketRecordArraySchema)
|
||||
}
|
||||
|
||||
public async listContacts(input: ResPartnerReadInput): Promise<ResPartnerReadOutput> {
|
||||
return this._postJson('/json/2/res.partner/read', input, odooRecordArraySchema)
|
||||
}
|
||||
|
||||
public async listLeads(input: CrmLeadReadInput): Promise<CrmLeadReadOutput> {
|
||||
return this._postJson('/json/2/crm.lead/read', input, odooRecordArraySchema)
|
||||
}
|
||||
|
||||
public async listTickets(input: HelpdeskTicketReadInput): Promise<HelpdeskTicketReadOutput> {
|
||||
return this._postJson('/json/2/helpdesk.ticket/read', input, helpdeskTicketRecordArraySchema)
|
||||
}
|
||||
|
||||
public async createContact(input: ResPartnerCreateInput): Promise<ResPartnerCreateOutput> {
|
||||
const { values, ...rest } = input
|
||||
const ids = await this._postJson('/json/2/res.partner/create', { ...rest, vals_list: [values] }, createdIdSchema)
|
||||
return ids[0]
|
||||
}
|
||||
|
||||
public async createLead(input: CrmLeadCreateInput): Promise<CrmLeadCreateOutput> {
|
||||
const { values, ...rest } = input
|
||||
const ids = await this._postJson('/json/2/crm.lead/create', { ...rest, vals_list: [values] }, createdIdSchema)
|
||||
|
||||
return ids[0]
|
||||
}
|
||||
|
||||
public async createTicket(input: HelpdeskTicketCreateInput): Promise<HelpdeskTicketCreateOutput> {
|
||||
const { values, ...rest } = input
|
||||
const ids = await this._postJson(
|
||||
'/json/2/helpdesk.ticket/create',
|
||||
{ ...rest, vals_list: [values] },
|
||||
createdIdSchema
|
||||
)
|
||||
|
||||
return ids[0]
|
||||
}
|
||||
|
||||
public async updateContacts(input: ResPartnerWriteInput): Promise<ResPartnerWriteOutput> {
|
||||
const { values, ...rest } = input
|
||||
|
||||
return this._postJson('/json/2/res.partner/write', { ...rest, vals: values }, booleanSchema)
|
||||
}
|
||||
|
||||
public async updateLeads(input: CrmLeadWriteInput): Promise<CrmLeadWriteOutput> {
|
||||
const { values, ...rest } = input
|
||||
|
||||
return this._postJson('/json/2/crm.lead/write', { ...rest, vals: values }, booleanSchema)
|
||||
}
|
||||
|
||||
public async updateTickets(input: HelpdeskTicketWriteInput): Promise<HelpdeskTicketWriteOutput> {
|
||||
const { values, ...rest } = input
|
||||
|
||||
return this._postJson('/json/2/helpdesk.ticket/write', { ...rest, vals: values }, booleanSchema)
|
||||
}
|
||||
|
||||
public async deleteContacts(input: ResPartnerUnlinkInput): Promise<ResPartnerUnlinkOutput> {
|
||||
return this._postJson('/json/2/res.partner/unlink', input, booleanSchema)
|
||||
}
|
||||
|
||||
public async deleteLeads(input: CrmLeadUnlinkInput): Promise<CrmLeadUnlinkOutput> {
|
||||
return this._postJson('/json/2/crm.lead/unlink', input, booleanSchema)
|
||||
}
|
||||
|
||||
public async deleteTickets(input: HelpdeskTicketUnlinkInput): Promise<HelpdeskTicketUnlinkOutput> {
|
||||
return this._postJson('/json/2/helpdesk.ticket/unlink', input, booleanSchema)
|
||||
}
|
||||
}
|
||||
|
||||
export default OdooClient
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createActionWrapper, createAsyncFnWrapperWithErrorRedaction } from '@botpress/common'
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { OdooClient } from '../OdooClient'
|
||||
import { createOdooRuntimeError } from './errors'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const wrapAction: typeof _wrapAction = (meta, actionImpl) =>
|
||||
_wrapAction(meta, (props) => {
|
||||
const logger = props.logger.forBot()
|
||||
|
||||
logger.debug(`Running action "${meta.actionName}" [bot id: ${props.ctx.botId}]`, { input: props.input })
|
||||
|
||||
return _wrapAsyncFnWithTryCatch(() => {
|
||||
const output = actionImpl(props as Parameters<typeof actionImpl>[0], props.input)
|
||||
|
||||
return output.catch((thrown) => {
|
||||
if (!(thrown instanceof sdk.RuntimeError)) {
|
||||
logger.warn(`Action Error: ${meta.errorMessage}`, {
|
||||
error: thrown instanceof Error ? thrown.message : String(thrown),
|
||||
})
|
||||
}
|
||||
|
||||
throw thrown
|
||||
}) as typeof output
|
||||
}, `Action Error: ${meta.errorMessage}`)()
|
||||
})
|
||||
|
||||
const _wrapAction = createActionWrapper<bp.IntegrationProps>()({
|
||||
toolFactories: {
|
||||
odooClient: ({ ctx }) =>
|
||||
new OdooClient(ctx.configuration.url, ctx.configuration.apiKey, ctx.configuration.database),
|
||||
},
|
||||
extraMetadata: {} as {
|
||||
errorMessage: string
|
||||
},
|
||||
})
|
||||
|
||||
const _wrapAsyncFnWithTryCatch = createAsyncFnWrapperWithErrorRedaction((error: Error) => {
|
||||
if (error instanceof sdk.RuntimeError) {
|
||||
return error
|
||||
}
|
||||
|
||||
return createOdooRuntimeError(error)
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
|
||||
export const getErrorMessage = (thrown: unknown): string => (thrown instanceof Error ? thrown.message : String(thrown))
|
||||
|
||||
const fieldLookupActionByModel: Record<string, string> = {
|
||||
'crm.lead': 'listLeadFields',
|
||||
'res.partner': 'listContactFields',
|
||||
}
|
||||
|
||||
export const createOdooRuntimeError = (thrown: unknown): sdk.RuntimeError => {
|
||||
const message = getErrorMessage(thrown)
|
||||
const invalidFieldMatch = /Invalid field '([^']+)' (?:on|in) '([^']+)'/.exec(message)
|
||||
|
||||
if (invalidFieldMatch) {
|
||||
const [, field, model] = invalidFieldMatch
|
||||
const fieldLookupAction = model ? fieldLookupActionByModel[model] : undefined
|
||||
const fieldLookupSuggestion = fieldLookupAction
|
||||
? `call ${fieldLookupAction} to see the available fields for this Odoo database`
|
||||
: 'call the matching field-list action to see the available fields for this Odoo database'
|
||||
|
||||
return new sdk.RuntimeError(
|
||||
`Invalid Odoo field "${field}" for model "${model}". Remove this field from the request or ${fieldLookupSuggestion}.`
|
||||
)
|
||||
}
|
||||
|
||||
return new sdk.RuntimeError(message)
|
||||
}
|
||||
|
||||
export const isActiveUserLinkedContactError = (message: string): boolean =>
|
||||
message.includes('You cannot delete contacts linked to an active user')
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const createContact = wrapAction(
|
||||
{ actionName: 'createContact', errorMessage: 'Failed to create Odoo contact' },
|
||||
async ({ odooClient }, input) => {
|
||||
const id = await odooClient.createContact(input)
|
||||
|
||||
return { id }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { z } from '@botpress/sdk'
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
import { getErrorMessage, isActiveUserLinkedContactError } from '../../errors'
|
||||
|
||||
type NotDeletedContact = {
|
||||
id: number
|
||||
name?: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
type DeletableContact = {
|
||||
id: number
|
||||
name?: string
|
||||
}
|
||||
|
||||
const getContactOwnerId = (contact: Record<string, unknown>): number | undefined => {
|
||||
const owner = contact.user_id
|
||||
|
||||
const isNumberArray = z.array(z.number()).safeParse(owner)
|
||||
if (isNumberArray.success) {
|
||||
return isNumberArray.data[0]
|
||||
}
|
||||
|
||||
const isNumber = z.number().safeParse(owner)
|
||||
if (isNumber.success) {
|
||||
return isNumber.data
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getContactName = (contact: Record<string, unknown>): string | undefined =>
|
||||
typeof contact.name === 'string' ? contact.name : undefined
|
||||
|
||||
const getDeleteContactsMessage = (deletedIds: number[], notDeletedContacts: NotDeletedContact[]): string => {
|
||||
if (notDeletedContacts.length === 0) {
|
||||
return `Deleted ${deletedIds.length} Odoo contact${deletedIds.length === 1 ? '' : 's'}.`
|
||||
}
|
||||
|
||||
const notDeletedContactIds = notDeletedContacts.map(({ id }) => id).join(', ')
|
||||
const deletedMessage =
|
||||
deletedIds.length === 0
|
||||
? 'No Odoo contacts were deleted.'
|
||||
: `Deleted ${deletedIds.length} Odoo contact${deletedIds.length === 1 ? '' : 's'}: ${deletedIds.join(', ')}.`
|
||||
|
||||
return `${deletedMessage} Could not delete ${notDeletedContacts.length} Odoo contact${
|
||||
notDeletedContacts.length === 1 ? '' : 's'
|
||||
}: ${notDeletedContactIds}.`
|
||||
}
|
||||
|
||||
const getNotDeletedContactsDetails = (notDeletedContacts: NotDeletedContact[]): string =>
|
||||
notDeletedContacts.map(({ id, reason }) => `Contact ${id}: ${reason}`).join(' ')
|
||||
|
||||
const deleteContactsIndividually = async (
|
||||
odooClient: {
|
||||
deleteContacts: (input: { ids: number[]; context?: Record<string, unknown> }) => Promise<boolean>
|
||||
},
|
||||
contacts: DeletableContact[],
|
||||
context: Record<string, unknown> | undefined
|
||||
): Promise<{ deletedIds: number[]; notDeletedContacts: NotDeletedContact[] }> => {
|
||||
const deletedIds: number[] = []
|
||||
const notDeletedContacts: NotDeletedContact[] = []
|
||||
|
||||
for (const { id, name } of contacts) {
|
||||
try {
|
||||
const success = await odooClient.deleteContacts({ ids: [id], context })
|
||||
|
||||
if (success) {
|
||||
deletedIds.push(id)
|
||||
} else {
|
||||
notDeletedContacts.push({
|
||||
id,
|
||||
name,
|
||||
reason: 'Odoo returned false while deleting this contact. Verify the contact ID and user permissions.',
|
||||
})
|
||||
}
|
||||
} catch (thrown) {
|
||||
const reason = getErrorMessage(thrown)
|
||||
|
||||
notDeletedContacts.push({
|
||||
id,
|
||||
name,
|
||||
reason: isActiveUserLinkedContactError(reason)
|
||||
? 'Contact is linked to an active Odoo user and cannot be deleted.'
|
||||
: reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { deletedIds, notDeletedContacts }
|
||||
}
|
||||
|
||||
export const deleteContacts = wrapAction(
|
||||
{ actionName: 'deleteContacts', errorMessage: 'Failed to delete Odoo contacts' },
|
||||
async ({ odooClient }, input) => {
|
||||
const contacts = await odooClient.listContacts({
|
||||
ids: input.ids,
|
||||
fields: ['id', 'name', 'user_id'],
|
||||
context: input.context,
|
||||
})
|
||||
const contactsById = new Map(
|
||||
contacts.flatMap((contact) => (typeof contact.id === 'number' ? [[contact.id, contact]] : []))
|
||||
)
|
||||
const deletedIds: number[] = []
|
||||
const notDeletedContacts: NotDeletedContact[] = []
|
||||
const deletableContacts: DeletableContact[] = []
|
||||
|
||||
for (const id of input.ids) {
|
||||
const contact = contactsById.get(id)
|
||||
|
||||
if (!contact) {
|
||||
notDeletedContacts.push({ id, reason: 'Contact was not found in Odoo.' })
|
||||
continue
|
||||
}
|
||||
|
||||
const name = getContactName(contact)
|
||||
const ownerId = getContactOwnerId(contact)
|
||||
|
||||
if (ownerId !== input.ownerId) {
|
||||
notDeletedContacts.push({
|
||||
id,
|
||||
name,
|
||||
reason:
|
||||
ownerId === undefined
|
||||
? 'Contact is not assigned to an owner.'
|
||||
: `Contact is owned by Odoo user ${ownerId}, not Odoo user ${input.ownerId}.`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
deletableContacts.push({ id, name })
|
||||
}
|
||||
|
||||
if (deletableContacts.length > 0) {
|
||||
try {
|
||||
const success = await odooClient.deleteContacts({
|
||||
ids: deletableContacts.map(({ id }) => id),
|
||||
context: input.context,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
deletedIds.push(...deletableContacts.map(({ id }) => id))
|
||||
} else {
|
||||
const individualResult = await deleteContactsIndividually(odooClient, deletableContacts, input.context)
|
||||
|
||||
deletedIds.push(...individualResult.deletedIds)
|
||||
notDeletedContacts.push(...individualResult.notDeletedContacts)
|
||||
}
|
||||
} catch {
|
||||
const individualResult = await deleteContactsIndividually(odooClient, deletableContacts, input.context)
|
||||
|
||||
deletedIds.push(...individualResult.deletedIds)
|
||||
notDeletedContacts.push(...individualResult.notDeletedContacts)
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedIds.length === 0 && notDeletedContacts.length > 0) {
|
||||
throw new sdk.RuntimeError(
|
||||
`${getDeleteContactsMessage(deletedIds, notDeletedContacts)} ${getNotDeletedContactsDetails(
|
||||
notDeletedContacts
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
deletedIds,
|
||||
notDeletedContacts,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
export { createContact } from './createContact'
|
||||
export { deleteContacts } from './deleteContacts'
|
||||
export { listContactFields } from './listContactFields'
|
||||
export { listContacts } from './listContacts'
|
||||
export { searchContacts } from './searchContacts'
|
||||
export { updateContacts } from './updateContacts'
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const listContactFields = wrapAction(
|
||||
{ actionName: 'listContactFields', errorMessage: 'Failed to list Odoo contact fields' },
|
||||
async ({ odooClient }, input) => {
|
||||
const fields = await odooClient.listFields('Contact', input)
|
||||
|
||||
return { fields }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const listContacts = wrapAction(
|
||||
{ actionName: 'listContacts', errorMessage: 'Failed to list Odoo contacts' },
|
||||
async ({ odooClient }, input) => {
|
||||
const records = await odooClient.listContacts(input)
|
||||
|
||||
return { records }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const searchContacts = wrapAction(
|
||||
{ actionName: 'searchContacts', errorMessage: 'Failed to search Odoo contacts' },
|
||||
async ({ odooClient }, input) => {
|
||||
const records = await odooClient.searchContacts(input)
|
||||
|
||||
return { records }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const updateContacts = wrapAction(
|
||||
{ actionName: 'updateContacts', errorMessage: 'Failed to update Odoo contacts' },
|
||||
async ({ odooClient }, input) => {
|
||||
const success = await odooClient.updateContacts(input)
|
||||
|
||||
if (!success) {
|
||||
throw new sdk.RuntimeError(
|
||||
`Odoo returned false while updating contact IDs ${input.ids.join(
|
||||
', '
|
||||
)}. The update was not applied; verify the contact IDs, field names, values, and user permissions.`
|
||||
)
|
||||
}
|
||||
|
||||
return { updatedIds: input.ids }
|
||||
}
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const getCurrentUser = wrapAction(
|
||||
{ actionName: 'getCurrentUser', errorMessage: 'Failed to get current Odoo user' },
|
||||
async ({ odooClient }) => {
|
||||
const id = await odooClient.getCurrentUserId()
|
||||
|
||||
return { id }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export { getCurrentUser } from './getCurrentUser'
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
createContact,
|
||||
deleteContacts,
|
||||
listContactFields,
|
||||
listContacts,
|
||||
searchContacts,
|
||||
updateContacts,
|
||||
} from './contacts'
|
||||
import { getCurrentUser } from './current-user'
|
||||
import { createLead, deleteLeads, listLeadFields, listLeads, searchLeads, updateLeads } from './leads'
|
||||
import { createTicket, deleteTickets, listTicketFields, listTickets, searchTickets, updateTickets } from './tickets'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default {
|
||||
getCurrentUser,
|
||||
listContactFields,
|
||||
listLeadFields,
|
||||
listTicketFields,
|
||||
searchLeads,
|
||||
searchTickets,
|
||||
listLeads,
|
||||
listTickets,
|
||||
createLead,
|
||||
createTicket,
|
||||
updateLeads,
|
||||
updateTickets,
|
||||
deleteLeads,
|
||||
deleteTickets,
|
||||
searchContacts,
|
||||
listContacts,
|
||||
createContact,
|
||||
updateContacts,
|
||||
deleteContacts,
|
||||
} as const satisfies bp.IntegrationProps['actions']
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const createLead = wrapAction(
|
||||
{ actionName: 'createLead', errorMessage: 'Failed to create Odoo lead' },
|
||||
async ({ odooClient }, input) => {
|
||||
const id = await odooClient.createLead(input)
|
||||
|
||||
return { id }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,168 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { z } from '@botpress/sdk'
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
import { getErrorMessage } from '../../errors'
|
||||
|
||||
type NotDeletedLead = {
|
||||
id: number
|
||||
name?: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
type DeletableLead = {
|
||||
id: number
|
||||
name?: string
|
||||
}
|
||||
|
||||
const getLeadOwnerId = (lead: Record<string, unknown>): number | undefined => {
|
||||
const owner = lead.user_id
|
||||
|
||||
const isNumberArray = z.array(z.number()).safeParse(owner)
|
||||
if (isNumberArray.success) {
|
||||
return isNumberArray.data[0]
|
||||
}
|
||||
|
||||
const isNumber = z.number().safeParse(owner)
|
||||
if (isNumber.success) {
|
||||
return isNumber.data
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getLeadName = (lead: Record<string, unknown>): string | undefined =>
|
||||
typeof lead.name === 'string' ? lead.name : undefined
|
||||
|
||||
const getDeleteLeadsMessage = (deletedIds: number[], notDeletedLeads: NotDeletedLead[]): string => {
|
||||
if (notDeletedLeads.length === 0) {
|
||||
return `Deleted ${deletedIds.length} Odoo lead${deletedIds.length === 1 ? '' : 's'}.`
|
||||
}
|
||||
|
||||
const notDeletedLeadIds = notDeletedLeads.map(({ id }) => id).join(', ')
|
||||
const deletedMessage =
|
||||
deletedIds.length === 0
|
||||
? 'No Odoo leads were deleted.'
|
||||
: `Deleted ${deletedIds.length} Odoo lead${deletedIds.length === 1 ? '' : 's'}: ${deletedIds.join(', ')}.`
|
||||
|
||||
return `${deletedMessage} Could not delete ${notDeletedLeads.length} Odoo lead${
|
||||
notDeletedLeads.length === 1 ? '' : 's'
|
||||
}: ${notDeletedLeadIds}.`
|
||||
}
|
||||
|
||||
const getNotDeletedLeadsDetails = (notDeletedLeads: NotDeletedLead[]): string =>
|
||||
notDeletedLeads.map(({ id, reason }) => `Lead ${id}: ${reason}`).join(' ')
|
||||
|
||||
const deleteLeadsIndividually = async (
|
||||
odooClient: {
|
||||
deleteLeads: (input: { ids: number[]; context?: Record<string, unknown> }) => Promise<boolean>
|
||||
},
|
||||
leads: DeletableLead[],
|
||||
context: Record<string, unknown> | undefined
|
||||
): Promise<{ deletedIds: number[]; notDeletedLeads: NotDeletedLead[] }> => {
|
||||
const deletedIds: number[] = []
|
||||
const notDeletedLeads: NotDeletedLead[] = []
|
||||
|
||||
for (const { id, name } of leads) {
|
||||
try {
|
||||
const success = await odooClient.deleteLeads({ ids: [id], context })
|
||||
|
||||
if (success) {
|
||||
deletedIds.push(id)
|
||||
} else {
|
||||
notDeletedLeads.push({
|
||||
id,
|
||||
name,
|
||||
reason: 'Odoo returned false while deleting this lead. Verify the lead ID and user permissions.',
|
||||
})
|
||||
}
|
||||
} catch (thrown) {
|
||||
notDeletedLeads.push({ id, name, reason: getErrorMessage(thrown) })
|
||||
}
|
||||
}
|
||||
|
||||
return { deletedIds, notDeletedLeads }
|
||||
}
|
||||
|
||||
async function deleteDeletableLeads(
|
||||
odooClient: {
|
||||
deleteLeads: (input: { ids: number[]; context?: Record<string, unknown> }) => Promise<boolean>
|
||||
},
|
||||
leads: DeletableLead[],
|
||||
context: Record<string, unknown> | undefined
|
||||
): Promise<{ deletedIds: number[]; notDeletedLeads: NotDeletedLead[] }> {
|
||||
if (leads.length === 0) {
|
||||
return { deletedIds: [], notDeletedLeads: [] }
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await odooClient.deleteLeads({
|
||||
ids: leads.map(({ id }) => id),
|
||||
context,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
return { deletedIds: leads.map(({ id }) => id), notDeletedLeads: [] }
|
||||
}
|
||||
} catch {
|
||||
// Fall back to individual deletes so we can report which leads failed.
|
||||
}
|
||||
|
||||
return deleteLeadsIndividually(odooClient, leads, context)
|
||||
}
|
||||
|
||||
export const deleteLeads = wrapAction(
|
||||
{ actionName: 'deleteLeads', errorMessage: 'Failed to delete Odoo leads' },
|
||||
async ({ odooClient }, input) => {
|
||||
const leads = await odooClient.listLeads({
|
||||
ids: input.ids,
|
||||
fields: ['id', 'name', 'user_id'],
|
||||
context: input.context,
|
||||
})
|
||||
const leadsById = new Map(leads.flatMap((lead) => (typeof lead.id === 'number' ? [[lead.id, lead]] : [])))
|
||||
const deletedIds: number[] = []
|
||||
const notDeletedLeads: NotDeletedLead[] = []
|
||||
const deletableLeads: DeletableLead[] = []
|
||||
|
||||
for (const id of input.ids) {
|
||||
const lead = leadsById.get(id)
|
||||
|
||||
if (!lead) {
|
||||
notDeletedLeads.push({ id, reason: 'Lead was not found in Odoo.' })
|
||||
continue
|
||||
}
|
||||
|
||||
const name = getLeadName(lead)
|
||||
const ownerId = getLeadOwnerId(lead)
|
||||
|
||||
if (ownerId !== input.ownerId) {
|
||||
notDeletedLeads.push({
|
||||
id,
|
||||
name,
|
||||
reason:
|
||||
ownerId === undefined
|
||||
? 'Lead is not assigned to an owner.'
|
||||
: `Lead is owned by Odoo user ${ownerId}, not Odoo user ${input.ownerId}.`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
deletableLeads.push({ id, name })
|
||||
}
|
||||
|
||||
const deleteResult = await deleteDeletableLeads(odooClient, deletableLeads, input.context)
|
||||
|
||||
deletedIds.push(...deleteResult.deletedIds)
|
||||
notDeletedLeads.push(...deleteResult.notDeletedLeads)
|
||||
|
||||
if (deletedIds.length === 0 && notDeletedLeads.length > 0) {
|
||||
throw new sdk.RuntimeError(
|
||||
`${getDeleteLeadsMessage(deletedIds, notDeletedLeads)} ${getNotDeletedLeadsDetails(notDeletedLeads)}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
deletedIds,
|
||||
notDeletedLeads,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
export { createLead } from './createLead'
|
||||
export { deleteLeads } from './deleteLeads'
|
||||
export { listLeadFields } from './listLeadFields'
|
||||
export { listLeads } from './listLeads'
|
||||
export { searchLeads } from './searchLeads'
|
||||
export { updateLeads } from './updateLeads'
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const listLeadFields = wrapAction(
|
||||
{ actionName: 'listLeadFields', errorMessage: 'Failed to list Odoo lead fields' },
|
||||
async ({ odooClient }, input) => {
|
||||
const fields = await odooClient.listLeadFields(input)
|
||||
|
||||
return { fields }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const listLeads = wrapAction(
|
||||
{ actionName: 'listLeads', errorMessage: 'Failed to list Odoo leads' },
|
||||
async ({ odooClient }, input) => {
|
||||
const records = await odooClient.listLeads(input)
|
||||
|
||||
return { records }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const searchLeads = wrapAction(
|
||||
{ actionName: 'searchLeads', errorMessage: 'Failed to search Odoo leads' },
|
||||
async ({ odooClient }, input) => {
|
||||
const records = await odooClient.searchLeads(input)
|
||||
|
||||
return { records }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const updateLeads = wrapAction(
|
||||
{ actionName: 'updateLeads', errorMessage: 'Failed to update Odoo leads' },
|
||||
async ({ odooClient }, input) => {
|
||||
const success = await odooClient.updateLeads(input)
|
||||
|
||||
if (!success) {
|
||||
throw new sdk.RuntimeError(
|
||||
`Odoo returned false while updating lead IDs ${input.ids.join(
|
||||
', '
|
||||
)}. The update was not applied; verify the lead IDs, field names, values, and user permissions.`
|
||||
)
|
||||
}
|
||||
|
||||
return { updatedIds: input.ids }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const createTicket = wrapAction(
|
||||
{ actionName: 'createTicket', errorMessage: 'Failed to create Odoo ticket' },
|
||||
async ({ odooClient }, input) => {
|
||||
const id = await odooClient.createTicket(input)
|
||||
|
||||
return { id }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const deleteTickets = wrapAction(
|
||||
{ actionName: 'deleteTickets', errorMessage: 'Failed to delete Odoo tickets' },
|
||||
async ({ odooClient }, input) => {
|
||||
const success = await odooClient.deleteTickets(input)
|
||||
|
||||
if (!success) {
|
||||
throw new sdk.RuntimeError(
|
||||
`Odoo returned false while deleting ticket IDs ${input.ids.join(
|
||||
', '
|
||||
)}. Verify the ticket IDs and user permissions.`
|
||||
)
|
||||
}
|
||||
|
||||
return { deletedIds: input.ids }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
export { createTicket } from './createTicket'
|
||||
export { deleteTickets } from './deleteTickets'
|
||||
export { listTicketFields } from './listTicketFields'
|
||||
export { listTickets } from './listTickets'
|
||||
export { searchTickets } from './searchTickets'
|
||||
export { updateTickets } from './updateTickets'
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const listTicketFields = wrapAction(
|
||||
{ actionName: 'listTicketFields', errorMessage: 'Failed to list Odoo ticket fields' },
|
||||
async ({ odooClient }, input) => {
|
||||
const fields = await odooClient.listTicketFields(input)
|
||||
|
||||
return { fields }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const listTickets = wrapAction(
|
||||
{ actionName: 'listTickets', errorMessage: 'Failed to list Odoo tickets' },
|
||||
async ({ odooClient }, input) => {
|
||||
const records = await odooClient.listTickets(input)
|
||||
|
||||
return { records }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const searchTickets = wrapAction(
|
||||
{ actionName: 'searchTickets', errorMessage: 'Failed to search Odoo tickets' },
|
||||
async ({ odooClient }, input) => {
|
||||
const records = await odooClient.searchTickets(input)
|
||||
|
||||
return { records }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import { wrapAction } from '../../action-wrapper'
|
||||
|
||||
export const updateTickets = wrapAction(
|
||||
{ actionName: 'updateTickets', errorMessage: 'Failed to update Odoo tickets' },
|
||||
async ({ odooClient }, input) => {
|
||||
const success = await odooClient.updateTickets(input)
|
||||
|
||||
if (!success) {
|
||||
throw new sdk.RuntimeError(
|
||||
`Odoo returned false while updating ticket IDs ${input.ids.join(
|
||||
', '
|
||||
)}. The update was not applied; verify the ticket IDs, field names, values, and user permissions.`
|
||||
)
|
||||
}
|
||||
|
||||
return { updatedIds: input.ids }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { OdooContext, OdooDomain, OdooRecord } from './odoo.types'
|
||||
|
||||
export type Model = 'Lead' | 'Contact' | 'Ticket'
|
||||
|
||||
export type GetFieldsRequest = {
|
||||
allfields?: string[]
|
||||
attributes?: string[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type GetFieldsOutput = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* POST /json/2/res.users/context_get
|
||||
*/
|
||||
export type ResUsersContextGetOutput = OdooContext & {
|
||||
uid: number
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /json/2/res.partner/search_read
|
||||
*/
|
||||
export type ResPartnerSearchReadInput = {
|
||||
domain?: OdooDomain
|
||||
fields?: string[]
|
||||
offset?: number
|
||||
limit?: number
|
||||
order?: string
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type ResPartnerSearchReadOutput = OdooRecord[]
|
||||
|
||||
/**
|
||||
* POST /json/2/res.partner/read
|
||||
*/
|
||||
export type ResPartnerReadInput = {
|
||||
ids: number[]
|
||||
fields?: string[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type ResPartnerReadOutput = OdooRecord[]
|
||||
|
||||
/**
|
||||
* POST /json/2/res.partner/create
|
||||
*/
|
||||
export type ResPartnerCreateInput = {
|
||||
values: Record<string, unknown>
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type ResPartnerCreateOutput = number
|
||||
|
||||
/**
|
||||
* POST /json/2/res.partner/write
|
||||
*/
|
||||
export type ResPartnerWriteInput = {
|
||||
ids: number[]
|
||||
values: Record<string, unknown>
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type ResPartnerWriteOutput = boolean
|
||||
|
||||
/**
|
||||
* POST /json/2/res.partner/unlink
|
||||
*/
|
||||
export type ResPartnerUnlinkInput = {
|
||||
ids: number[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type ResPartnerUnlinkOutput = boolean
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './contact.types'
|
||||
export * from './lead.types'
|
||||
export * from './ticket.types'
|
||||
export * from './odoo.types'
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { OdooContext, OdooDomain, OdooRecord } from './odoo.types'
|
||||
|
||||
/**
|
||||
* POST /json/2/crm.lead/fields_get
|
||||
*/
|
||||
export type CrmLeadFieldsGetInput = {
|
||||
allfields?: string[]
|
||||
attributes?: string[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type CrmLeadFieldMetadata = Record<string, unknown>
|
||||
|
||||
export type CrmLeadFieldsGetOutput = Record<string, CrmLeadFieldMetadata>
|
||||
|
||||
/**
|
||||
* POST /json/2/crm.lead/search_read
|
||||
*/
|
||||
export type CrmLeadSearchReadInput = {
|
||||
domain?: OdooDomain
|
||||
fields?: string[]
|
||||
offset?: number
|
||||
limit?: number
|
||||
order?: string
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type CrmLeadSearchReadOutput = OdooRecord[]
|
||||
|
||||
/**
|
||||
* POST /json/2/crm.lead/read
|
||||
*/
|
||||
export type CrmLeadReadInput = {
|
||||
ids: number[]
|
||||
fields?: string[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type CrmLeadReadOutput = OdooRecord[]
|
||||
|
||||
/**
|
||||
* POST /json/2/crm.lead/create
|
||||
*/
|
||||
export type CrmLeadCreateInput = {
|
||||
values: {
|
||||
name: string
|
||||
email_from?: string
|
||||
phone?: string
|
||||
mobile?: string
|
||||
contact_name?: string
|
||||
partner_name?: string
|
||||
partner_id?: number
|
||||
stage_id?: number
|
||||
user_id?: number
|
||||
team_id?: number
|
||||
type?: 'lead' | 'opportunity'
|
||||
probability?: number
|
||||
expected_revenue?: number
|
||||
description?: string
|
||||
referred?: string
|
||||
source_id?: number
|
||||
medium_id?: number
|
||||
campaign_id?: number
|
||||
[field: string]: unknown
|
||||
}
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type CrmLeadCreateOutput = number
|
||||
|
||||
/**
|
||||
* POST /json/2/crm.lead/write
|
||||
*/
|
||||
export type CrmLeadWriteInput = {
|
||||
ids: number[]
|
||||
values: Partial<CrmLeadCreateInput['values']>
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type CrmLeadWriteOutput = boolean
|
||||
|
||||
/**
|
||||
* POST /json/2/crm.lead/unlink
|
||||
*/
|
||||
export type CrmLeadUnlinkInput = {
|
||||
ids: number[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type CrmLeadUnlinkOutput = boolean
|
||||
@@ -0,0 +1,9 @@
|
||||
export type OdooContext = Record<string, unknown>
|
||||
|
||||
export type OdooDomainCondition = unknown[]
|
||||
export type OdooDomainOperator = '&' | '|' | '!'
|
||||
export type OdooDomain = Array<OdooDomainCondition | OdooDomainOperator>
|
||||
|
||||
export type OdooRecord = Record<string, unknown> & { id?: number }
|
||||
|
||||
export type OdooMany2One = [id: number, displayName: string]
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { OdooContext, OdooDomain, OdooRecord } from './odoo.types'
|
||||
|
||||
export type HelpdeskTicketRecord = OdooRecord & { id: number }
|
||||
|
||||
/**
|
||||
* POST /json/2/helpdesk.ticket/fields_get
|
||||
*/
|
||||
export type HelpdeskTicketFieldsGetInput = {
|
||||
allfields?: string[]
|
||||
attributes?: string[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type HelpdeskTicketFieldsGetOutput = Record<string, Record<string, unknown>>
|
||||
|
||||
/**
|
||||
* POST /json/2/helpdesk.ticket/search_read
|
||||
*/
|
||||
export type HelpdeskTicketSearchReadInput = {
|
||||
domain?: OdooDomain
|
||||
fields?: string[]
|
||||
offset?: number
|
||||
limit?: number
|
||||
order?: string
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type HelpdeskTicketSearchReadOutput = HelpdeskTicketRecord[]
|
||||
|
||||
/**
|
||||
* POST /json/2/helpdesk.ticket/read
|
||||
*/
|
||||
export type HelpdeskTicketReadInput = {
|
||||
ids: number[]
|
||||
fields?: string[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type HelpdeskTicketReadOutput = HelpdeskTicketRecord[]
|
||||
|
||||
/**
|
||||
* POST /json/2/helpdesk.ticket/create
|
||||
*/
|
||||
export type HelpdeskTicketCreateInput = {
|
||||
values: {
|
||||
name: string
|
||||
description?: string
|
||||
partner_id?: number
|
||||
partner_name?: string
|
||||
partner_email?: string
|
||||
email_cc?: string
|
||||
team_id?: number
|
||||
user_id?: number
|
||||
stage_id?: number
|
||||
ticket_type_id?: number
|
||||
priority?: '0' | '1' | '2' | '3'
|
||||
tag_ids?: number[]
|
||||
company_id?: number
|
||||
[field: string]: unknown
|
||||
}
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type HelpdeskTicketCreateOutput = number
|
||||
|
||||
/**
|
||||
* POST /json/2/helpdesk.ticket/write
|
||||
*/
|
||||
export type HelpdeskTicketWriteInput = {
|
||||
ids: number[]
|
||||
values: Record<string, unknown>
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type HelpdeskTicketWriteOutput = boolean
|
||||
|
||||
/**
|
||||
* POST /json/2/helpdesk.ticket/unlink
|
||||
*/
|
||||
export type HelpdeskTicketUnlinkInput = {
|
||||
ids: number[]
|
||||
context?: OdooContext
|
||||
}
|
||||
|
||||
export type HelpdeskTicketUnlinkOutput = boolean
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user