@bradtech/sales-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.dist +18 -0
- package/AGENT.md +58 -0
- package/HOWTO.md +72 -0
- package/LICENSE +8 -0
- package/LLM.md +37 -0
- package/README.md +100 -0
- package/bin/activate_skills.sh +54 -0
- package/bin/lm_studio_agent.ts +172 -0
- package/bin/publish-prepare.cjs +69 -0
- package/bun.lock +262 -0
- package/package.json +35 -0
- package/skills/actions/sync-meetings-to-odoo/SKILL.md +97 -0
- package/skills/actions/sync-meetings-to-odoo/scripts/sync_meetings_cli.ts +235 -0
- package/skills/vendor/brevo/SKILL.md +83 -0
- package/skills/vendor/brevo/cli.ts +167 -0
- package/skills/vendor/google/SKILL.md +66 -0
- package/skills/vendor/google/calendar.ts +150 -0
- package/skills/vendor/google/cli.ts +74 -0
- package/skills/vendor/odoo/SKILL.md +116 -0
- package/skills/vendor/odoo/cli.ts +566 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { CliCommand } from '@quatrain/cli';
|
|
2
|
+
import { XmlRpcClient } from '@quatrain/api-xmlrpc';
|
|
3
|
+
import { Skills } from '@quatrain/skills';
|
|
4
|
+
|
|
5
|
+
export class OdooClient {
|
|
6
|
+
private db: string;
|
|
7
|
+
private username: string;
|
|
8
|
+
private password: string;
|
|
9
|
+
private uid!: number;
|
|
10
|
+
private commonClient: XmlRpcClient;
|
|
11
|
+
private objectClient: XmlRpcClient;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
const urlStr = process.env.ODOO_URL;
|
|
15
|
+
this.db = process.env.ODOO_DB || '';
|
|
16
|
+
this.username = process.env.ODOO_USER || '';
|
|
17
|
+
this.password = process.env.ODOO_PASSWORD || '';
|
|
18
|
+
|
|
19
|
+
if (!urlStr || !this.db || !this.username || !this.password) {
|
|
20
|
+
Skills.error(
|
|
21
|
+
'Error: ODOO_URL, ODOO_DB, ODOO_USER, and ODOO_PASSWORD must be defined in your .env file.'
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(urlStr);
|
|
28
|
+
const isHttps = url.protocol === 'https:';
|
|
29
|
+
const port = url.port ? parseInt(url.port, 10) : (isHttps ? 443 : 80);
|
|
30
|
+
|
|
31
|
+
this.commonClient = new XmlRpcClient({
|
|
32
|
+
host: url.hostname,
|
|
33
|
+
port,
|
|
34
|
+
path: '/xmlrpc/2/common',
|
|
35
|
+
secure: isHttps
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.objectClient = new XmlRpcClient({
|
|
39
|
+
host: url.hostname,
|
|
40
|
+
port,
|
|
41
|
+
path: '/xmlrpc/2/object',
|
|
42
|
+
secure: isHttps
|
|
43
|
+
});
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
Skills.error(`Error parsing Odoo URL: ${err.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async authenticate(): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
const value = await this.commonClient.methodCall(
|
|
53
|
+
'authenticate',
|
|
54
|
+
[this.db, this.username, this.password, {}]
|
|
55
|
+
);
|
|
56
|
+
if (!value) {
|
|
57
|
+
throw new Error('Authentication failed. Check your Odoo credentials.');
|
|
58
|
+
}
|
|
59
|
+
this.uid = value;
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
Skills.error(`Odoo Authentication Error: ${err.message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async executeKw(model: string, method: string, args: any[], kwargs: any = {}): Promise<any> {
|
|
67
|
+
return this.objectClient.methodCall(
|
|
68
|
+
'execute_kw',
|
|
69
|
+
[this.db, this.uid, this.password, model, method, args, kwargs]
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async findCountryId(countryNameOrCode?: string): Promise<number | null> {
|
|
74
|
+
if (!countryNameOrCode) return null;
|
|
75
|
+
try {
|
|
76
|
+
const domain = [
|
|
77
|
+
'|',
|
|
78
|
+
['name', '=ilike', countryNameOrCode],
|
|
79
|
+
['code', '=ilike', countryNameOrCode]
|
|
80
|
+
];
|
|
81
|
+
const countryIds = await this.executeKw('res.country', 'search', [domain]);
|
|
82
|
+
return countryIds.length > 0 ? countryIds[0] : null;
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
Skills.warn(`Warning searching country '${countryNameOrCode}': ${err.message}`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async findActivityTypeId(nameQuery = 'todo'): Promise<number | null> {
|
|
90
|
+
try {
|
|
91
|
+
let domain = [
|
|
92
|
+
'|',
|
|
93
|
+
['name', '=ilike', nameQuery],
|
|
94
|
+
['name', '=ilike', 'to do']
|
|
95
|
+
];
|
|
96
|
+
let typeIds = await this.executeKw('mail.activity.type', 'search', [domain]);
|
|
97
|
+
|
|
98
|
+
if (typeIds.length === 0 && nameQuery === 'todo') {
|
|
99
|
+
const domainFr = [['name', 'ilike', 'faire']];
|
|
100
|
+
typeIds = await this.executeKw('mail.activity.type', 'search', [domainFr]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeIds.length === 0) {
|
|
104
|
+
typeIds = await this.executeKw('mail.activity.type', 'search', [[]]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return typeIds.length > 0 ? typeIds[0] : null;
|
|
108
|
+
} catch (err: any) {
|
|
109
|
+
Skills.warn(`Warning searching activity type '${nameQuery}': ${err.message}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async findModelId(modelName: string): Promise<number | null> {
|
|
115
|
+
try {
|
|
116
|
+
const modelIds = await this.executeKw('ir.model', 'search', [[['model', '=', modelName]]]);
|
|
117
|
+
return modelIds.length > 0 ? modelIds[0] : null;
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
Skills.warn(`Warning searching model '${modelName}': ${err.message}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async createCompany(
|
|
125
|
+
name: string,
|
|
126
|
+
email?: string,
|
|
127
|
+
phone?: string,
|
|
128
|
+
city?: string,
|
|
129
|
+
country?: string,
|
|
130
|
+
street?: string
|
|
131
|
+
) {
|
|
132
|
+
const values: any = {
|
|
133
|
+
name,
|
|
134
|
+
is_company: true,
|
|
135
|
+
company_type: 'company'
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (email) values.email = email;
|
|
139
|
+
if (phone) values.phone = phone;
|
|
140
|
+
if (city) values.city = city;
|
|
141
|
+
if (street) values.street = street;
|
|
142
|
+
|
|
143
|
+
const countryId = await this.findCountryId(country);
|
|
144
|
+
if (countryId) values.country_id = countryId;
|
|
145
|
+
|
|
146
|
+
const companyId = await this.executeKw('res.partner', 'create', [values]);
|
|
147
|
+
return {
|
|
148
|
+
id: companyId,
|
|
149
|
+
name,
|
|
150
|
+
type: 'company',
|
|
151
|
+
city: city || null,
|
|
152
|
+
street: street || null,
|
|
153
|
+
country_id: countryId
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async createContact(
|
|
158
|
+
name: string,
|
|
159
|
+
companyId?: number,
|
|
160
|
+
email?: string,
|
|
161
|
+
phone?: string,
|
|
162
|
+
city?: string,
|
|
163
|
+
country?: string,
|
|
164
|
+
functionName?: string,
|
|
165
|
+
street?: string
|
|
166
|
+
) {
|
|
167
|
+
const values: any = {
|
|
168
|
+
name,
|
|
169
|
+
is_company: false,
|
|
170
|
+
company_type: 'person'
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (companyId) values.parent_id = companyId;
|
|
174
|
+
if (email) values.email = email;
|
|
175
|
+
if (phone) values.phone = phone;
|
|
176
|
+
if (city) values.city = city;
|
|
177
|
+
if (functionName) values.function = functionName;
|
|
178
|
+
if (street) values.street = street;
|
|
179
|
+
|
|
180
|
+
const countryId = await this.findCountryId(country);
|
|
181
|
+
if (countryId) values.country_id = countryId;
|
|
182
|
+
|
|
183
|
+
const contactId = await this.executeKw('res.partner', 'create', [values]);
|
|
184
|
+
return {
|
|
185
|
+
id: contactId,
|
|
186
|
+
name,
|
|
187
|
+
company_id: companyId || null,
|
|
188
|
+
type: 'contact',
|
|
189
|
+
city: city || null,
|
|
190
|
+
street: street || null,
|
|
191
|
+
country_id: countryId,
|
|
192
|
+
function: functionName || null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async updatePartner(partnerId: number, rawValues: any) {
|
|
197
|
+
const values = { ...rawValues };
|
|
198
|
+
if ('country' in values && values.country) {
|
|
199
|
+
const countryId = await this.findCountryId(values.country);
|
|
200
|
+
if (countryId) {
|
|
201
|
+
values.country_id = countryId;
|
|
202
|
+
}
|
|
203
|
+
delete values.country;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Clean up undefined or null values
|
|
207
|
+
const cleanValues: any = {};
|
|
208
|
+
for (const [k, v] of Object.entries(values)) {
|
|
209
|
+
if (v !== undefined && v !== null) {
|
|
210
|
+
cleanValues[k] = v;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (Object.keys(cleanValues).length > 0) {
|
|
215
|
+
await this.executeKw('res.partner', 'write', [[partnerId], cleanValues]);
|
|
216
|
+
}
|
|
217
|
+
return { id: partnerId, updated_fields: Object.keys(cleanValues) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async createOpportunity(name: string, partnerId: number, revenue = 0.0, description?: string) {
|
|
221
|
+
const values: any = {
|
|
222
|
+
name,
|
|
223
|
+
partner_id: partnerId,
|
|
224
|
+
type: 'opportunity'
|
|
225
|
+
};
|
|
226
|
+
if (revenue) values.planned_revenue = revenue;
|
|
227
|
+
if (description) values.description = description;
|
|
228
|
+
|
|
229
|
+
const opportunityId = await this.executeKw('crm.lead', 'create', [values]);
|
|
230
|
+
return {
|
|
231
|
+
id: opportunityId,
|
|
232
|
+
name,
|
|
233
|
+
partner_id: partnerId,
|
|
234
|
+
planned_revenue: revenue
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async createMeeting(name: string, startDatetime: string, durationHours = 1.0, partnerIds?: number[]) {
|
|
239
|
+
// Computes stop datetime to prevent Odoo timezone/validation error
|
|
240
|
+
// Start datetime expected in format YYYY-MM-DD HH:MM:SS
|
|
241
|
+
const startStrNorm = startDatetime.replace(' ', 'T');
|
|
242
|
+
const startDt = new Date(startStrNorm);
|
|
243
|
+
if (isNaN(startDt.getTime())) {
|
|
244
|
+
Skills.error(`Invalid start datetime format: ${startDatetime}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const endDt = new Date(startDt.getTime() + durationHours * 60 * 60 * 1000);
|
|
249
|
+
|
|
250
|
+
// Format back to YYYY-MM-DD HH:MM:SS for Odoo
|
|
251
|
+
const formatOdooDate = (date: Date) => {
|
|
252
|
+
const pad = (num: number) => String(num).padStart(2, '0');
|
|
253
|
+
const yyyy = date.getUTCFullYear();
|
|
254
|
+
const mm = pad(date.getUTCMonth() + 1);
|
|
255
|
+
const dd = pad(date.getUTCDate());
|
|
256
|
+
const hh = pad(date.getUTCHours());
|
|
257
|
+
const min = pad(date.getUTCMinutes());
|
|
258
|
+
const ss = pad(date.getUTCSeconds());
|
|
259
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}`;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const startOdoo = formatOdooDate(startDt);
|
|
263
|
+
const stopOdoo = formatOdooDate(endDt);
|
|
264
|
+
|
|
265
|
+
const values: any = {
|
|
266
|
+
name,
|
|
267
|
+
start: startOdoo,
|
|
268
|
+
stop: stopOdoo,
|
|
269
|
+
duration: durationHours
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (partnerIds && partnerIds.length > 0) {
|
|
273
|
+
values.partner_ids = [[6, 0, partnerIds]];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const eventId = await this.executeKw('calendar.event', 'create', [values]);
|
|
277
|
+
return {
|
|
278
|
+
id: eventId,
|
|
279
|
+
name,
|
|
280
|
+
start: startOdoo,
|
|
281
|
+
stop: stopOdoo,
|
|
282
|
+
duration: durationHours,
|
|
283
|
+
partner_ids: partnerIds || null
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async createActivity(resModel: string, resId: number, summary: string, note?: string, activityTypeName = 'todo') {
|
|
288
|
+
const activityTypeId = await this.findActivityTypeId(activityTypeName);
|
|
289
|
+
const resModelId = await this.findModelId(resModel);
|
|
290
|
+
|
|
291
|
+
const values: any = {
|
|
292
|
+
res_model: resModel,
|
|
293
|
+
res_id: resId,
|
|
294
|
+
summary
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (resModelId) values.res_model_id = resModelId;
|
|
298
|
+
if (note) values.note = note;
|
|
299
|
+
if (activityTypeId) values.activity_type_id = activityTypeId;
|
|
300
|
+
|
|
301
|
+
const activityId = await this.executeKw('mail.activity', 'create', [values]);
|
|
302
|
+
return {
|
|
303
|
+
id: activityId,
|
|
304
|
+
res_model: resModel,
|
|
305
|
+
res_id: resId,
|
|
306
|
+
summary,
|
|
307
|
+
activity_type_id: activityTypeId
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async resetCrm() {
|
|
312
|
+
const leadIds = await this.executeKw('crm.lead', 'search', [[]]);
|
|
313
|
+
if (leadIds.length > 0) {
|
|
314
|
+
await this.executeKw('crm.lead', 'unlink', [leadIds]);
|
|
315
|
+
}
|
|
316
|
+
return { deleted_ids: leadIds, count: leadIds.length };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async searchOpportunities(query?: string, limit = 100) {
|
|
320
|
+
const domain: any[] = [['type', '=', 'opportunity']];
|
|
321
|
+
if (query) {
|
|
322
|
+
domain.push(['name', 'ilike', query]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const fields = [
|
|
326
|
+
'id',
|
|
327
|
+
'name',
|
|
328
|
+
'partner_id',
|
|
329
|
+
'stage_id',
|
|
330
|
+
'planned_revenue',
|
|
331
|
+
'probability',
|
|
332
|
+
'create_date'
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
return this.executeKw('crm.lead', 'search_read', [domain], { fields, limit });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async searchPartners(query?: string, isCompany?: boolean, limit = 100) {
|
|
339
|
+
const domain: any[] = [];
|
|
340
|
+
if (isCompany !== undefined) {
|
|
341
|
+
domain.push(['is_company', '=', isCompany]);
|
|
342
|
+
}
|
|
343
|
+
if (query) {
|
|
344
|
+
domain.push(['name', 'ilike', query]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const fields = [
|
|
348
|
+
'id',
|
|
349
|
+
'name',
|
|
350
|
+
'is_company',
|
|
351
|
+
'email',
|
|
352
|
+
'phone',
|
|
353
|
+
'city',
|
|
354
|
+
'country_id'
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
return this.executeKw('res.partner', 'search_read', [domain], { fields, limit });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function main() {
|
|
362
|
+
const program = new CliCommand();
|
|
363
|
+
program
|
|
364
|
+
.name('odoo_cli')
|
|
365
|
+
.description('Odoo ERP Integration CLI wrapper');
|
|
366
|
+
|
|
367
|
+
// Command: create-company
|
|
368
|
+
program
|
|
369
|
+
.command('create-company')
|
|
370
|
+
.description('Create a new company partner')
|
|
371
|
+
.requiredOption('--name <name>', 'Company name')
|
|
372
|
+
.option('--email <email>', 'Company email')
|
|
373
|
+
.option('--phone <phone>', 'Company phone')
|
|
374
|
+
.option('--city <city>', 'Company city')
|
|
375
|
+
.option('--country <country>', 'Company country name or code')
|
|
376
|
+
.option('--street <street>', 'Company street address')
|
|
377
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
378
|
+
.action(async (options) => {
|
|
379
|
+
const client = new OdooClient();
|
|
380
|
+
await client.authenticate();
|
|
381
|
+
const result = await client.createCompany(
|
|
382
|
+
options.name,
|
|
383
|
+
options.email,
|
|
384
|
+
options.phone,
|
|
385
|
+
options.city,
|
|
386
|
+
options.country,
|
|
387
|
+
options.street
|
|
388
|
+
);
|
|
389
|
+
await Skills.writeOutput(result, options.output);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Command: create-contact
|
|
393
|
+
program
|
|
394
|
+
.command('create-contact')
|
|
395
|
+
.description('Create a new individual contact')
|
|
396
|
+
.requiredOption('--name <name>', 'Contact name')
|
|
397
|
+
.option('--company-id <id>', 'Parent company ID', (val) => parseInt(val, 10))
|
|
398
|
+
.option('--email <email>', 'Contact email')
|
|
399
|
+
.option('--phone <phone>', 'Contact phone')
|
|
400
|
+
.option('--city <city>', 'Contact city')
|
|
401
|
+
.option('--country <country>', 'Contact country name or code')
|
|
402
|
+
.option('--street <street>', 'Contact street address')
|
|
403
|
+
.option('--function <job>', 'Contact job position/function')
|
|
404
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
405
|
+
.action(async (options) => {
|
|
406
|
+
const client = new OdooClient();
|
|
407
|
+
await client.authenticate();
|
|
408
|
+
const result = await client.createContact(
|
|
409
|
+
options.name,
|
|
410
|
+
options.companyId,
|
|
411
|
+
options.email,
|
|
412
|
+
options.phone,
|
|
413
|
+
options.city,
|
|
414
|
+
options.country,
|
|
415
|
+
options.function,
|
|
416
|
+
options.street
|
|
417
|
+
);
|
|
418
|
+
await Skills.writeOutput(result, options.output);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Command: update-partner
|
|
422
|
+
program
|
|
423
|
+
.command('update-partner')
|
|
424
|
+
.description('Update an existing partner (contact or company)')
|
|
425
|
+
.requiredOption('--id <id>', 'Partner ID to update', (val) => parseInt(val, 10))
|
|
426
|
+
.option('--name <name>', 'Updated name')
|
|
427
|
+
.option('--email <email>', 'Updated email')
|
|
428
|
+
.option('--phone <phone>', 'Updated phone')
|
|
429
|
+
.option('--city <city>', 'Updated city')
|
|
430
|
+
.option('--country <country>', 'Updated country')
|
|
431
|
+
.option('--street <street>', 'Updated street address')
|
|
432
|
+
.option('--function <job>', 'Updated job position/function')
|
|
433
|
+
.option('--company-id <id>', 'Updated parent company ID', (val) => parseInt(val, 10))
|
|
434
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
435
|
+
.action(async (options) => {
|
|
436
|
+
const client = new OdooClient();
|
|
437
|
+
await client.authenticate();
|
|
438
|
+
const values = {
|
|
439
|
+
name: options.name,
|
|
440
|
+
email: options.email,
|
|
441
|
+
phone: options.phone,
|
|
442
|
+
city: options.city,
|
|
443
|
+
country: options.country,
|
|
444
|
+
street: options.street,
|
|
445
|
+
function: options.function,
|
|
446
|
+
parent_id: options.companyId
|
|
447
|
+
};
|
|
448
|
+
const result = await client.updatePartner(options.id, values);
|
|
449
|
+
await Skills.writeOutput(result, options.output);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Command: create-opportunity
|
|
453
|
+
program
|
|
454
|
+
.command('create-opportunity')
|
|
455
|
+
.description('Create a CRM opportunity')
|
|
456
|
+
.requiredOption('--name <subject>', 'Opportunity subject/name')
|
|
457
|
+
.requiredOption('--partner-id <id>', 'Partner ID linked to the opportunity', (val) => parseInt(val, 10))
|
|
458
|
+
.option('--revenue <amount>', 'Estimated revenue', (val) => parseFloat(val), 0.0)
|
|
459
|
+
.option('--description <notes>', 'Internal description notes')
|
|
460
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
461
|
+
.action(async (options) => {
|
|
462
|
+
const client = new OdooClient();
|
|
463
|
+
await client.authenticate();
|
|
464
|
+
const result = await client.createOpportunity(
|
|
465
|
+
options.name,
|
|
466
|
+
options.partnerId,
|
|
467
|
+
options.revenue,
|
|
468
|
+
options.description
|
|
469
|
+
);
|
|
470
|
+
await Skills.writeOutput(result, options.output);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Command: create-meeting
|
|
474
|
+
program
|
|
475
|
+
.command('create-meeting')
|
|
476
|
+
.description('Create a calendar event / meeting')
|
|
477
|
+
.requiredOption('--name <subject>', 'Meeting subject')
|
|
478
|
+
.requiredOption('--start <datetime>', 'Start datetime YYYY-MM-DD HH:MM:SS')
|
|
479
|
+
.option('--duration <hours>', 'Duration in hours', (val) => parseFloat(val), 1.0)
|
|
480
|
+
.option('--partner-ids <ids>', 'Comma-separated partner IDs', (val) => val.split(',').map((x) => parseInt(x.trim(), 10)))
|
|
481
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
482
|
+
.action(async (options) => {
|
|
483
|
+
const client = new OdooClient();
|
|
484
|
+
await client.authenticate();
|
|
485
|
+
const result = await client.createMeeting(
|
|
486
|
+
options.name,
|
|
487
|
+
options.start,
|
|
488
|
+
options.duration,
|
|
489
|
+
options.partnerIds
|
|
490
|
+
);
|
|
491
|
+
await Skills.writeOutput(result, options.output);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Command: create-activity
|
|
495
|
+
program
|
|
496
|
+
.command('create-activity')
|
|
497
|
+
.description('Create a planned activity on a model')
|
|
498
|
+
.requiredOption('--model <model>', 'Target model (res.partner or crm.lead)')
|
|
499
|
+
.requiredOption('--res-id <id>', 'Target record ID', (val) => parseInt(val, 10))
|
|
500
|
+
.requiredOption('--summary <title>', 'Activity summary')
|
|
501
|
+
.option('--note <html-text>', 'HTML formatted activity note')
|
|
502
|
+
.option('--type <type>', 'Activity type (todo, email, call)', 'todo')
|
|
503
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
504
|
+
.action(async (options) => {
|
|
505
|
+
const client = new OdooClient();
|
|
506
|
+
await client.authenticate();
|
|
507
|
+
const result = await client.createActivity(
|
|
508
|
+
options.model,
|
|
509
|
+
options.resId,
|
|
510
|
+
options.summary,
|
|
511
|
+
options.note,
|
|
512
|
+
options.type
|
|
513
|
+
);
|
|
514
|
+
await Skills.writeOutput(result, options.output);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Command: reset-crm
|
|
518
|
+
program
|
|
519
|
+
.command('reset-crm')
|
|
520
|
+
.description('Delete all CRM leads/opportunities')
|
|
521
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
522
|
+
.action(async (options) => {
|
|
523
|
+
const client = new OdooClient();
|
|
524
|
+
await client.authenticate();
|
|
525
|
+
const result = await client.resetCrm();
|
|
526
|
+
await Skills.writeOutput(result, options.output);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Command: search-opportunities
|
|
530
|
+
program
|
|
531
|
+
.command('search-opportunities')
|
|
532
|
+
.description('Search and list CRM opportunities')
|
|
533
|
+
.option('--query <text>', 'Search query matching opportunity name')
|
|
534
|
+
.requiredOption('--limit <number>', 'Max results limit', (val) => parseInt(val, 10))
|
|
535
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
536
|
+
.action(async (options) => {
|
|
537
|
+
const client = new OdooClient();
|
|
538
|
+
await client.authenticate();
|
|
539
|
+
const result = await client.searchOpportunities(options.query, options.limit);
|
|
540
|
+
await Skills.writeOutput(result, options.output);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Command: search-partners
|
|
544
|
+
program
|
|
545
|
+
.command('search-partners')
|
|
546
|
+
.description('Search and list partners (contacts or companies)')
|
|
547
|
+
.option('--query <text>', 'Search query matching partner name')
|
|
548
|
+
.option('--is-company <boolean>', 'Filter by company status (true or false)', (val) => val === 'true')
|
|
549
|
+
.requiredOption('--limit <number>', 'Max results limit', (val) => parseInt(val, 10))
|
|
550
|
+
.requiredOption('--output <path>', 'Output JSON file path')
|
|
551
|
+
.action(async (options) => {
|
|
552
|
+
const client = new OdooClient();
|
|
553
|
+
await client.authenticate();
|
|
554
|
+
const result = await client.searchPartners(options.query, options.isCompany, options.limit);
|
|
555
|
+
await Skills.writeOutput(result, options.output);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
await program.parseAsync(process.argv);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (import.meta.main) {
|
|
562
|
+
main().catch((err) => {
|
|
563
|
+
Skills.error(`Unexpected process error: ${err.message}`);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
});
|
|
566
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"moduleDetection": "force",
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@quatrain/skills": ["../../QUATRAIN/Core/packages/skills/src/index.ts"],
|
|
15
|
+
"@quatrain/log": ["../../QUATRAIN/Core/packages/log/src/index.ts"],
|
|
16
|
+
"@quatrain/core": ["../../QUATRAIN/Core/packages/core/src/index.ts"],
|
|
17
|
+
"@quatrain/types": ["../../QUATRAIN/Core/packages/types/src/index.ts"],
|
|
18
|
+
"@quatrain/api-client": ["../../QUATRAIN/Core/packages/api-client/src/index.ts"],
|
|
19
|
+
"@quatrain/cli": ["../../QUATRAIN/Core/packages/cli/src/index.ts"],
|
|
20
|
+
"@quatrain/api-xmlrpc": ["../../QUATRAIN/Core/packages/api-xmlrpc/src/index.ts"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"include": ["skills/**/*"]
|
|
24
|
+
}
|