@bergetai/odoo-cli 0.5.3 → 0.6.1
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/README.md +7 -0
- package/dist/cli.js +68 -1
- package/dist/commands/notes.js +158 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,12 @@ odoo crm teams delete 5 --dry-run
|
|
|
57
57
|
# Internal notes (NEVER sends email to customer)
|
|
58
58
|
odoo helpdesk tickets add-note 398 --body "Internal note text"
|
|
59
59
|
odoo helpdesk tickets add-note 398 --body-file note.md
|
|
60
|
+
|
|
61
|
+
# Log note correction (message-update / message-delete; comment-type only)
|
|
62
|
+
odoo crm records message-update 270799 --body-file fixed.html --dry-run
|
|
63
|
+
odoo crm records message-update 270799 --body-file fixed.html
|
|
64
|
+
odoo crm records message-delete 253924 258332 --dry-run
|
|
65
|
+
odoo crm records message-delete 253924 258332
|
|
60
66
|
```
|
|
61
67
|
|
|
62
68
|
Configuration precedence: flags > environment (`ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_API_KEY`) > `$XDG_CONFIG_HOME/odoo/config.json`. The API key is never accepted as a flag value.
|
|
@@ -75,6 +81,7 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
|
|
|
75
81
|
- Writes (`create`, `update`, `delete`) — single-call atomic batch `write`, client-side no-op diffing, delete convergence with `skipped` ids, post-failure "record may have been created" hint on `create`
|
|
76
82
|
- `--dry-run` for all mutations — full validation, resolution, and before/after diffs without mutating
|
|
77
83
|
- Internal notes (`add-note`) — creates internal Log Notes only (subtype_id=2, message_type=comment), **NEVER sends email to customer**, by design prevents accidental customer communication
|
|
84
|
+
- Log note correction (`message-update`, `message-delete`) — repair formatting or remove duplicate log notes; strictly limited to comment-type messages (emails/notifications refused by design), full `--dry-run` support, unknown delete ids skipped
|
|
78
85
|
- Config sources and precedence with atomic `0600` config writes, XML-RPC transport with timeouts and HTTPS enforcement, and an auth failure taxonomy (db missing / bad credentials / unreachable)
|
|
79
86
|
|
|
80
87
|
## Compatibility
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { authLoginCommand, authStatusCommand } from "./commands/auth.js";
|
|
|
4
4
|
import { leadsCreateCommand, leadsDeleteCommand, leadsListCommand, leadsShowCommand, leadsUpdateCommand, crmLeads, } from "./commands/crm.js";
|
|
5
5
|
import { teamsCreateCommand, teamsDeleteCommand, teamsListCommand, teamsShowCommand, teamsUpdateCommand, crmTeams, } from "./commands/teams.js";
|
|
6
6
|
import { ticketsCreateCommand, ticketsDeleteCommand, ticketsListCommand, ticketsShowCommand, ticketsUpdateCommand, helpdeskTickets, } from "./commands/helpdesk.js";
|
|
7
|
-
import { addNoteCommand, listMessagesCommand, } from "./commands/notes.js";
|
|
7
|
+
import { addNoteCommand, deleteMessageCommand, listMessagesCommand, updateMessageCommand, } from "./commands/notes.js";
|
|
8
8
|
import { activitiesListCommand, activityDoneCommand, activityUpdateCommand, scheduleCommand, } from "./commands/activities.js";
|
|
9
9
|
import { createTaskCommand, projectListCommand, projectShowCommand, tasksListCommand, tasksShowCommand, } from "./commands/project.js";
|
|
10
10
|
import { documentsDocuments, documentsListCommand, foldersListCommand, documentsShowCommand, documentsCreateCommand, documentsUpdateCommand, documentsDeleteCommand, treeCommand, uploadCommand, } from "./commands/documents.js";
|
|
@@ -177,6 +177,41 @@ Examples:
|
|
|
177
177
|
.action(async (id, opts) => {
|
|
178
178
|
await run((g) => listMessagesCommand(id, opts, g, "crm.lead"), program);
|
|
179
179
|
});
|
|
180
|
+
leads
|
|
181
|
+
.command("message-update")
|
|
182
|
+
.description("update the body of an existing log note (preview with --dry-run)")
|
|
183
|
+
.argument("<id>", "message id (see: records messages <id>)")
|
|
184
|
+
.option("--body <text>", "new body (HTML allowed)")
|
|
185
|
+
.option("--body-file <path>", "read new body from file ('-' for stdin)")
|
|
186
|
+
.addHelpText("after", `
|
|
187
|
+
Log notes (message_type "comment") and "Activities" traces (Meeting/To-Do
|
|
188
|
+
done — for correcting minutes after the fact) can be updated. Emails and other
|
|
189
|
+
notifications are refused by design. The message id and date are preserved.
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
# Preview a formatting repair
|
|
193
|
+
$ odoo crm records message-update 270799 --body-file fixed.html --dry-run
|
|
194
|
+
|
|
195
|
+
# Apply it
|
|
196
|
+
$ odoo crm records message-update 270799 --body-file fixed.html`)
|
|
197
|
+
.action(async (id, opts) => {
|
|
198
|
+
await run((g) => updateMessageCommand(id, opts, g, "crm.lead"), program);
|
|
199
|
+
});
|
|
200
|
+
leads
|
|
201
|
+
.command("message-delete")
|
|
202
|
+
.description("delete log notes, e.g. duplicate logs (preview with --dry-run; unknown ids are skipped)")
|
|
203
|
+
.argument("<id...>", "message ids (see: records messages <id>)")
|
|
204
|
+
.addHelpText("after", `
|
|
205
|
+
Only internal log notes (message_type "comment") can be deleted — emails and
|
|
206
|
+
notifications are refused by design. Deletion is irreversible: run --dry-run
|
|
207
|
+
first and consider consolidating content into the note you keep.
|
|
208
|
+
|
|
209
|
+
Examples:
|
|
210
|
+
$ odoo crm records message-delete 253924 258332 --dry-run
|
|
211
|
+
$ odoo crm records message-delete 253924 258332 --json`)
|
|
212
|
+
.action(async (ids) => {
|
|
213
|
+
await run((g) => deleteMessageCommand(ids, g, "crm.lead"), program);
|
|
214
|
+
});
|
|
180
215
|
leads
|
|
181
216
|
.command("schedule")
|
|
182
217
|
.description("schedule a new activity on a record (preview with --dry-run)")
|
|
@@ -400,6 +435,38 @@ Examples:
|
|
|
400
435
|
.action(async (id, opts) => {
|
|
401
436
|
await run((g) => listMessagesCommand(id, opts, g, "helpdesk.ticket"), program);
|
|
402
437
|
});
|
|
438
|
+
tickets
|
|
439
|
+
.command("message-update")
|
|
440
|
+
.description("update the body of an existing log note (preview with --dry-run)")
|
|
441
|
+
.argument("<id>", "message id (see: tickets messages <id>)")
|
|
442
|
+
.option("--body <text>", "new body (HTML allowed)")
|
|
443
|
+
.option("--body-file <path>", "read new body from file ('-' for stdin)")
|
|
444
|
+
.addHelpText("after", `
|
|
445
|
+
Log notes (message_type "comment") and "Activities" traces (Meeting/To-Do
|
|
446
|
+
done — for correcting minutes after the fact) can be updated. Emails and other
|
|
447
|
+
notifications are refused by design. The message id and date are preserved.
|
|
448
|
+
|
|
449
|
+
Examples:
|
|
450
|
+
$ odoo helpdesk tickets message-update 270799 --body-file fixed.html --dry-run
|
|
451
|
+
$ odoo helpdesk tickets message-update 270799 --body-file fixed.html`)
|
|
452
|
+
.action(async (id, opts) => {
|
|
453
|
+
await run((g) => updateMessageCommand(id, opts, g, "helpdesk.ticket"), program);
|
|
454
|
+
});
|
|
455
|
+
tickets
|
|
456
|
+
.command("message-delete")
|
|
457
|
+
.description("delete log notes, e.g. duplicate logs (preview with --dry-run; unknown ids are skipped)")
|
|
458
|
+
.argument("<id...>", "message ids (see: tickets messages <id>)")
|
|
459
|
+
.addHelpText("after", `
|
|
460
|
+
Only internal log notes (message_type "comment") can be deleted — emails and
|
|
461
|
+
notifications are refused by design. Deletion is irreversible: run --dry-run
|
|
462
|
+
first and consider consolidating content into the note you keep.
|
|
463
|
+
|
|
464
|
+
Examples:
|
|
465
|
+
$ odoo helpdesk tickets message-delete 253924 258332 --dry-run
|
|
466
|
+
$ odoo helpdesk tickets message-delete 253924 258332 --json`)
|
|
467
|
+
.action(async (ids) => {
|
|
468
|
+
await run((g) => deleteMessageCommand(ids, g, "helpdesk.ticket"), program);
|
|
469
|
+
});
|
|
403
470
|
const project = program
|
|
404
471
|
.command("project")
|
|
405
472
|
.description("Project app (project.project, project.task)");
|
package/dist/commands/notes.js
CHANGED
|
@@ -113,6 +113,164 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
113
113
|
console.log(` Type: Internal note (not sent to customer)`);
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Read a mail.message and enforce the safety rules shared by
|
|
118
|
+
* updateMessageCommand and deleteMessageCommand:
|
|
119
|
+
* - the message must exist
|
|
120
|
+
* - it must belong to the command's thread model (crm.lead / helpdesk.ticket)
|
|
121
|
+
* - for updates: it must be a comment (log note) OR an "Activities"
|
|
122
|
+
* notification (the "Meeting done"/"To-Do done" trace, whose embedded
|
|
123
|
+
* note may need correction after the activity is marked done)
|
|
124
|
+
* - for deletes: strictly comments — deleting activity traces would
|
|
125
|
+
* corrupt reporting (the done-message is all that remains of an activity)
|
|
126
|
+
*/
|
|
127
|
+
async function readLogNote(client, messageId, spec, opts = {}) {
|
|
128
|
+
const rows = (await client.executeKw("mail.message", "read", [[messageId]], {
|
|
129
|
+
fields: ["id", "model", "res_id", "body", "message_type", "subtype_id", "date"],
|
|
130
|
+
}));
|
|
131
|
+
if (!rows || rows.length === 0) {
|
|
132
|
+
throw notFoundError("message_not_found", `mail.message #${messageId} does not exist`, "check the message id (see: records messages <id>)");
|
|
133
|
+
}
|
|
134
|
+
const msg = rows[0];
|
|
135
|
+
if (msg.model !== spec.model) {
|
|
136
|
+
throw usageError(`mail.message #${messageId} belongs to ${msg.model}, not ${spec.model}`, `use the ${msg.model === "helpdesk.ticket" ? "helpdesk tickets" : "crm records"} command for this message`);
|
|
137
|
+
}
|
|
138
|
+
const subtypeName = Array.isArray(msg.subtype_id) ? msg.subtype_id[1] : undefined;
|
|
139
|
+
const isComment = msg.message_type === "comment";
|
|
140
|
+
const isActivityTrace = msg.message_type === "notification" && subtypeName === "Activities";
|
|
141
|
+
if (opts.forDelete ? !isComment : !(isComment || isActivityTrace)) {
|
|
142
|
+
const allowed = opts.forDelete
|
|
143
|
+
? 'only "comment" (log notes) can be deleted'
|
|
144
|
+
: 'only "comment" (log notes) and "Activities" traces can be modified';
|
|
145
|
+
throw usageError(`mail.message #${messageId} has type "${msg.message_type}"${subtypeName ? ` / subtype "${subtypeName}"` : ""} — ${allowed}`, "emails and system notifications are left untouched by design");
|
|
146
|
+
}
|
|
147
|
+
return msg;
|
|
148
|
+
}
|
|
149
|
+
function bodyPreview(body, max = 80) {
|
|
150
|
+
const text = body
|
|
151
|
+
.replace(/<[^>]*>/g, "")
|
|
152
|
+
.replace(/\s+/g, " ")
|
|
153
|
+
.trim();
|
|
154
|
+
return text.length > max ? `${text.substring(0, max)}...` : text;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Update the body of an existing internal log note (mail.message).
|
|
158
|
+
*
|
|
159
|
+
* Use case: repair formatting (escaped HTML, markdown, platform noise) or
|
|
160
|
+
* correct a note posted with wrong content. The message id and date are
|
|
161
|
+
* preserved — only the body changes.
|
|
162
|
+
*/
|
|
163
|
+
export async function updateMessageCommand(messageId, opts, globals, model) {
|
|
164
|
+
const spec = threadModel(model);
|
|
165
|
+
const id = Number.parseInt(messageId, 10);
|
|
166
|
+
if (Number.isNaN(id) || id <= 0) {
|
|
167
|
+
throw usageError(`invalid message id: ${messageId}`, "provide a positive integer");
|
|
168
|
+
}
|
|
169
|
+
const body = await readBody(opts);
|
|
170
|
+
const client = await buildClient(globals);
|
|
171
|
+
const msg = await readLogNote(client, id, spec);
|
|
172
|
+
if (globals.dryRun) {
|
|
173
|
+
const result = {
|
|
174
|
+
dry_run: true,
|
|
175
|
+
message_id: id,
|
|
176
|
+
model: msg.model,
|
|
177
|
+
res_id: msg.res_id,
|
|
178
|
+
old_body_preview: bodyPreview(msg.body),
|
|
179
|
+
new_body_preview: bodyPreview(body),
|
|
180
|
+
new_body_length: body.length,
|
|
181
|
+
};
|
|
182
|
+
if (globals.json) {
|
|
183
|
+
printJson(result);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(`[DRY RUN] Would update log note #${id} on ${msg.model} #${msg.res_id}:`);
|
|
187
|
+
console.log(` Old: ${result.old_body_preview}`);
|
|
188
|
+
console.log(` New: ${result.new_body_preview}`);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await client.executeKw("mail.message", "write", [[id], { body }]);
|
|
193
|
+
const result = {
|
|
194
|
+
message_id: id,
|
|
195
|
+
model: msg.model,
|
|
196
|
+
res_id: msg.res_id,
|
|
197
|
+
updated: true,
|
|
198
|
+
};
|
|
199
|
+
if (globals.json) {
|
|
200
|
+
printJson(result);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(`✓ Log note #${id} updated on ${msg.model} #${msg.res_id}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Delete internal log notes (mail.message) — e.g. duplicate logs of the
|
|
208
|
+
* same dialog. Unknown ids are skipped with a warning. Only comment-type
|
|
209
|
+
* messages can be deleted.
|
|
210
|
+
*/
|
|
211
|
+
export async function deleteMessageCommand(messageIds, globals, model) {
|
|
212
|
+
const spec = threadModel(model);
|
|
213
|
+
const client = await buildClient(globals);
|
|
214
|
+
const toDelete = [];
|
|
215
|
+
const skipped = [];
|
|
216
|
+
for (const raw of messageIds) {
|
|
217
|
+
const id = Number.parseInt(raw, 10);
|
|
218
|
+
if (Number.isNaN(id) || id <= 0) {
|
|
219
|
+
skipped.push({ id: raw, reason: "invalid id" });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
toDelete.push(await readLogNote(client, id, spec, { forDelete: true }));
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof Error && "exitCode" in error && error.exitCode === 4) {
|
|
227
|
+
skipped.push({ id: raw, reason: "not found" });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (globals.dryRun) {
|
|
234
|
+
const result = {
|
|
235
|
+
dry_run: true,
|
|
236
|
+
would_delete: toDelete.map((m) => ({
|
|
237
|
+
message_id: m.id,
|
|
238
|
+
res_id: m.res_id,
|
|
239
|
+
date: m.date,
|
|
240
|
+
body_preview: bodyPreview(m.body),
|
|
241
|
+
})),
|
|
242
|
+
skipped,
|
|
243
|
+
};
|
|
244
|
+
if (globals.json) {
|
|
245
|
+
printJson(result);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.log(`[DRY RUN] Would delete ${toDelete.length} log note(s):`);
|
|
249
|
+
for (const m of toDelete) {
|
|
250
|
+
console.log(` #${m.id} (${m.date}) ${bodyPreview(m.body)}`);
|
|
251
|
+
}
|
|
252
|
+
for (const s of skipped) {
|
|
253
|
+
console.log(` skipped #${s.id}: ${s.reason}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const deleted = [];
|
|
259
|
+
for (const m of toDelete) {
|
|
260
|
+
await client.executeKw("mail.message", "unlink", [[m.id]]);
|
|
261
|
+
deleted.push(m.id);
|
|
262
|
+
}
|
|
263
|
+
const result = { deleted, skipped };
|
|
264
|
+
if (globals.json) {
|
|
265
|
+
printJson(result);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.log(`✓ Deleted ${deleted.length} log note(s): ${deleted.join(", ") || "none"}`);
|
|
269
|
+
for (const s of skipped) {
|
|
270
|
+
console.log(` skipped #${s.id}: ${s.reason}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
116
274
|
/**
|
|
117
275
|
* List messages for a mail-thread record (emails, comments, notes).
|
|
118
276
|
*
|