@bergetai/odoo-cli 0.5.3 → 0.7.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 +9 -1
- package/dist/cli.js +74 -3
- package/dist/commands/notes.js +185 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,13 @@ 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
|
+
odoo crm records add-note 338 --body "Customer replied: ..." --author 40
|
|
61
|
+
|
|
62
|
+
# Log note correction (message-update / message-delete; comment-type only)
|
|
63
|
+
odoo crm records message-update 270799 --body-file fixed.html --dry-run
|
|
64
|
+
odoo crm records message-update 270799 --body-file fixed.html
|
|
65
|
+
odoo crm records message-delete 253924 258332 --dry-run
|
|
66
|
+
odoo crm records message-delete 253924 258332
|
|
60
67
|
```
|
|
61
68
|
|
|
62
69
|
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.
|
|
@@ -74,7 +81,8 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
|
|
|
74
81
|
- Documents surface (`documents`) — `tree` (hierarchical folder listing with `--count`/`--depth`/`--folder`), `records list/show` (folders excluded by default, `--type folder|binary|url|all`, `--folder`/`--owner`/`--partner`/`--tag`), `folders list`, `records create` (folder via `--folder-type folder`, link via `--link <url>`), `records update` (move via `--folder`), `records delete`, and `upload` (local file → base64 `datas`, MIME guessed from extension, `--dry-run` previews without sending bytes). Folders are `documents.document` rows with `type='folder'`; the hierarchy is `folder_id`. Module-not-installed error (exit 4)
|
|
75
82
|
- 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
83
|
- `--dry-run` for all mutations — full validation, resolution, and before/after diffs without mutating
|
|
77
|
-
- 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
|
+
- 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. Optional `--author <email|name|id>` attributes the note to the partner who actually sent the logged message (resolved via `res.partner`; ambiguous matches list candidates and never guess)
|
|
85
|
+
- 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
86
|
- 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
87
|
|
|
80
88
|
## 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";
|
|
@@ -139,6 +139,7 @@ Examples:
|
|
|
139
139
|
.argument("<id>", "record id")
|
|
140
140
|
.option("--body <text>", "note body (HTML allowed)")
|
|
141
141
|
.option("--body-file <path>", "read note body from file ('-' for stdin)")
|
|
142
|
+
.option("--author <email|name|id>", "note author (res.partner; default: your own partner)")
|
|
142
143
|
.addHelpText("after", `
|
|
143
144
|
IMPORTANT: This command creates INTERNAL notes only. These notes:
|
|
144
145
|
• Are NEVER sent to the customer via email
|
|
@@ -151,7 +152,8 @@ This is by design to prevent accidental customer emails.
|
|
|
151
152
|
Examples:
|
|
152
153
|
$ odoo crm records add-note 338 --body "Internal note text"
|
|
153
154
|
$ odoo crm records add-note 338 --body-file note.md
|
|
154
|
-
$ cat note.md | odoo crm records add-note 338 --body-file
|
|
155
|
+
$ cat note.md | odoo crm records add-note 338 --body-file -
|
|
156
|
+
$ odoo crm records add-note 338 --body "Kundens svar: ..." --author andreas@berget.ai`)
|
|
155
157
|
.action(async (id, opts) => {
|
|
156
158
|
await run((g) => addNoteCommand(id, opts, g, "crm.lead"), program);
|
|
157
159
|
});
|
|
@@ -177,6 +179,41 @@ Examples:
|
|
|
177
179
|
.action(async (id, opts) => {
|
|
178
180
|
await run((g) => listMessagesCommand(id, opts, g, "crm.lead"), program);
|
|
179
181
|
});
|
|
182
|
+
leads
|
|
183
|
+
.command("message-update")
|
|
184
|
+
.description("update the body of an existing log note (preview with --dry-run)")
|
|
185
|
+
.argument("<id>", "message id (see: records messages <id>)")
|
|
186
|
+
.option("--body <text>", "new body (HTML allowed)")
|
|
187
|
+
.option("--body-file <path>", "read new body from file ('-' for stdin)")
|
|
188
|
+
.addHelpText("after", `
|
|
189
|
+
Log notes (message_type "comment") and "Activities" traces (Meeting/To-Do
|
|
190
|
+
done — for correcting minutes after the fact) can be updated. Emails and other
|
|
191
|
+
notifications are refused by design. The message id and date are preserved.
|
|
192
|
+
|
|
193
|
+
Examples:
|
|
194
|
+
# Preview a formatting repair
|
|
195
|
+
$ odoo crm records message-update 270799 --body-file fixed.html --dry-run
|
|
196
|
+
|
|
197
|
+
# Apply it
|
|
198
|
+
$ odoo crm records message-update 270799 --body-file fixed.html`)
|
|
199
|
+
.action(async (id, opts) => {
|
|
200
|
+
await run((g) => updateMessageCommand(id, opts, g, "crm.lead"), program);
|
|
201
|
+
});
|
|
202
|
+
leads
|
|
203
|
+
.command("message-delete")
|
|
204
|
+
.description("delete log notes, e.g. duplicate logs (preview with --dry-run; unknown ids are skipped)")
|
|
205
|
+
.argument("<id...>", "message ids (see: records messages <id>)")
|
|
206
|
+
.addHelpText("after", `
|
|
207
|
+
Only internal log notes (message_type "comment") can be deleted — emails and
|
|
208
|
+
notifications are refused by design. Deletion is irreversible: run --dry-run
|
|
209
|
+
first and consider consolidating content into the note you keep.
|
|
210
|
+
|
|
211
|
+
Examples:
|
|
212
|
+
$ odoo crm records message-delete 253924 258332 --dry-run
|
|
213
|
+
$ odoo crm records message-delete 253924 258332 --json`)
|
|
214
|
+
.action(async (ids) => {
|
|
215
|
+
await run((g) => deleteMessageCommand(ids, g, "crm.lead"), program);
|
|
216
|
+
});
|
|
180
217
|
leads
|
|
181
218
|
.command("schedule")
|
|
182
219
|
.description("schedule a new activity on a record (preview with --dry-run)")
|
|
@@ -356,6 +393,7 @@ the raw form.`)
|
|
|
356
393
|
.argument("<id>", "ticket id")
|
|
357
394
|
.option("--body <text>", "note body (HTML allowed)")
|
|
358
395
|
.option("--body-file <path>", "read note body from file ('-' for stdin)")
|
|
396
|
+
.option("--author <email|name|id>", "note author (res.partner; default: your own partner)")
|
|
359
397
|
.addHelpText("after", `
|
|
360
398
|
IMPORTANT: This command creates INTERNAL notes only. These notes:
|
|
361
399
|
• Are NEVER sent to the customer via email
|
|
@@ -368,7 +406,8 @@ This is by design to prevent accidental customer emails.
|
|
|
368
406
|
Examples:
|
|
369
407
|
$ odoo helpdesk tickets add-note 398 --body "Internal note text"
|
|
370
408
|
$ odoo helpdesk tickets add-note 398 --body-file note.md
|
|
371
|
-
$ cat note.md | odoo helpdesk tickets add-note 398 --body-file
|
|
409
|
+
$ cat note.md | odoo helpdesk tickets add-note 398 --body-file -
|
|
410
|
+
$ odoo helpdesk tickets add-note 398 --body "Kundens svar: ..." --author kund@example.com`)
|
|
372
411
|
.action(async (id, opts) => {
|
|
373
412
|
await run((g) => addNoteCommand(id, opts, g, "helpdesk.ticket"), program);
|
|
374
413
|
});
|
|
@@ -400,6 +439,38 @@ Examples:
|
|
|
400
439
|
.action(async (id, opts) => {
|
|
401
440
|
await run((g) => listMessagesCommand(id, opts, g, "helpdesk.ticket"), program);
|
|
402
441
|
});
|
|
442
|
+
tickets
|
|
443
|
+
.command("message-update")
|
|
444
|
+
.description("update the body of an existing log note (preview with --dry-run)")
|
|
445
|
+
.argument("<id>", "message id (see: tickets messages <id>)")
|
|
446
|
+
.option("--body <text>", "new body (HTML allowed)")
|
|
447
|
+
.option("--body-file <path>", "read new body from file ('-' for stdin)")
|
|
448
|
+
.addHelpText("after", `
|
|
449
|
+
Log notes (message_type "comment") and "Activities" traces (Meeting/To-Do
|
|
450
|
+
done — for correcting minutes after the fact) can be updated. Emails and other
|
|
451
|
+
notifications are refused by design. The message id and date are preserved.
|
|
452
|
+
|
|
453
|
+
Examples:
|
|
454
|
+
$ odoo helpdesk tickets message-update 270799 --body-file fixed.html --dry-run
|
|
455
|
+
$ odoo helpdesk tickets message-update 270799 --body-file fixed.html`)
|
|
456
|
+
.action(async (id, opts) => {
|
|
457
|
+
await run((g) => updateMessageCommand(id, opts, g, "helpdesk.ticket"), program);
|
|
458
|
+
});
|
|
459
|
+
tickets
|
|
460
|
+
.command("message-delete")
|
|
461
|
+
.description("delete log notes, e.g. duplicate logs (preview with --dry-run; unknown ids are skipped)")
|
|
462
|
+
.argument("<id...>", "message ids (see: tickets messages <id>)")
|
|
463
|
+
.addHelpText("after", `
|
|
464
|
+
Only internal log notes (message_type "comment") can be deleted — emails and
|
|
465
|
+
notifications are refused by design. Deletion is irreversible: run --dry-run
|
|
466
|
+
first and consider consolidating content into the note you keep.
|
|
467
|
+
|
|
468
|
+
Examples:
|
|
469
|
+
$ odoo helpdesk tickets message-delete 253924 258332 --dry-run
|
|
470
|
+
$ odoo helpdesk tickets message-delete 253924 258332 --json`)
|
|
471
|
+
.action(async (ids) => {
|
|
472
|
+
await run((g) => deleteMessageCommand(ids, g, "helpdesk.ticket"), program);
|
|
473
|
+
});
|
|
403
474
|
const project = program
|
|
404
475
|
.command("project")
|
|
405
476
|
.description("Project app (project.project, project.task)");
|
package/dist/commands/notes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OdooClient } from "../client.js";
|
|
2
2
|
import { resolveConfig } from "../config.js";
|
|
3
3
|
import { notFoundError, usageError } from "../errors.js";
|
|
4
|
+
import { resolveReference } from "../model.js";
|
|
4
5
|
import { printJson, verbose as logVerbose } from "../output.js";
|
|
5
6
|
import { formatTable } from "../table.js";
|
|
6
7
|
const THREAD_MODELS = {
|
|
@@ -55,6 +56,21 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
55
56
|
if (!rows || rows.length === 0) {
|
|
56
57
|
throw notFoundError("record_not_found", `${spec.model} #${id} does not exist`, `check the ${spec.label} id`);
|
|
57
58
|
}
|
|
59
|
+
// Optional: attribute the note to the person who actually sent the
|
|
60
|
+
// logged message (e.g. a forwarded customer email). author_id is a
|
|
61
|
+
// res.partner — resolve by exact email, name, or numeric id. Without
|
|
62
|
+
// --author Odoo defaults to the authenticated user's partner.
|
|
63
|
+
let author;
|
|
64
|
+
if (opts.author) {
|
|
65
|
+
author = await resolveReference(client, "res.partner", opts.author, {
|
|
66
|
+
label: "author",
|
|
67
|
+
searchFields: ["name"],
|
|
68
|
+
exactFields: ["email"],
|
|
69
|
+
});
|
|
70
|
+
if (globals.verbose) {
|
|
71
|
+
logVerbose(`resolved --author "${opts.author}" -> [${author.id}, "${author.name}"]`, globals.verbose);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
58
74
|
// Create internal Log Note
|
|
59
75
|
// CRITICAL: These parameters ensure the note is INTERNAL ONLY
|
|
60
76
|
// - subtype_id=2: "Note" subtype (internal, not sent to customer)
|
|
@@ -66,6 +82,9 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
66
82
|
message_type: "comment",
|
|
67
83
|
subtype_id: 2, // Note subtype = internal only, NEVER sends email
|
|
68
84
|
};
|
|
85
|
+
if (author) {
|
|
86
|
+
noteParams.author_id = author.id;
|
|
87
|
+
}
|
|
69
88
|
if (globals.verbose) {
|
|
70
89
|
logVerbose(`Creating internal note for ${spec.model} #${id} (${rows[0].name})`, globals.verbose);
|
|
71
90
|
logVerbose(`Note parameters: ${JSON.stringify(noteParams, null, 2)}`, globals.verbose);
|
|
@@ -78,6 +97,7 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
78
97
|
record_name: rows[0].name,
|
|
79
98
|
note_type: "internal_log_note",
|
|
80
99
|
will_send_email: false,
|
|
100
|
+
author: author ? { id: author.id, name: author.name } : null,
|
|
81
101
|
body_preview: body.substring(0, 100) + (body.length > 100 ? "..." : ""),
|
|
82
102
|
body_length: body.length,
|
|
83
103
|
};
|
|
@@ -88,6 +108,9 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
88
108
|
console.log(`[DRY RUN] Would create internal Log Note:`);
|
|
89
109
|
console.log(` Record: ${spec.model} #${id} — ${rows[0].name}`);
|
|
90
110
|
console.log(` Type: Internal note (never sent to customer)`);
|
|
111
|
+
if (author) {
|
|
112
|
+
console.log(` Author: ${author.name} (${author.id})`);
|
|
113
|
+
}
|
|
91
114
|
console.log(` Body length: ${body.length} characters`);
|
|
92
115
|
console.log(` Body preview: ${result.body_preview}`);
|
|
93
116
|
}
|
|
@@ -103,6 +126,7 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
103
126
|
record_name: rows[0].name,
|
|
104
127
|
note_type: "internal_log_note",
|
|
105
128
|
email_sent: false,
|
|
129
|
+
author: author ? { id: author.id, name: author.name } : null,
|
|
106
130
|
};
|
|
107
131
|
if (globals.json) {
|
|
108
132
|
printJson(result);
|
|
@@ -111,6 +135,167 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
111
135
|
console.log(`✓ Internal Log Note created (ID: ${noteId})`);
|
|
112
136
|
console.log(` Record: ${spec.model} #${id} — ${rows[0].name}`);
|
|
113
137
|
console.log(` Type: Internal note (not sent to customer)`);
|
|
138
|
+
if (author) {
|
|
139
|
+
console.log(` Author: ${author.name} (${author.id})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Read a mail.message and enforce the safety rules shared by
|
|
145
|
+
* updateMessageCommand and deleteMessageCommand:
|
|
146
|
+
* - the message must exist
|
|
147
|
+
* - it must belong to the command's thread model (crm.lead / helpdesk.ticket)
|
|
148
|
+
* - for updates: it must be a comment (log note) OR an "Activities"
|
|
149
|
+
* notification (the "Meeting done"/"To-Do done" trace, whose embedded
|
|
150
|
+
* note may need correction after the activity is marked done)
|
|
151
|
+
* - for deletes: strictly comments — deleting activity traces would
|
|
152
|
+
* corrupt reporting (the done-message is all that remains of an activity)
|
|
153
|
+
*/
|
|
154
|
+
async function readLogNote(client, messageId, spec, opts = {}) {
|
|
155
|
+
const rows = (await client.executeKw("mail.message", "read", [[messageId]], {
|
|
156
|
+
fields: ["id", "model", "res_id", "body", "message_type", "subtype_id", "date"],
|
|
157
|
+
}));
|
|
158
|
+
if (!rows || rows.length === 0) {
|
|
159
|
+
throw notFoundError("message_not_found", `mail.message #${messageId} does not exist`, "check the message id (see: records messages <id>)");
|
|
160
|
+
}
|
|
161
|
+
const msg = rows[0];
|
|
162
|
+
if (msg.model !== spec.model) {
|
|
163
|
+
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`);
|
|
164
|
+
}
|
|
165
|
+
const subtypeName = Array.isArray(msg.subtype_id) ? msg.subtype_id[1] : undefined;
|
|
166
|
+
const isComment = msg.message_type === "comment";
|
|
167
|
+
const isActivityTrace = msg.message_type === "notification" && subtypeName === "Activities";
|
|
168
|
+
if (opts.forDelete ? !isComment : !(isComment || isActivityTrace)) {
|
|
169
|
+
const allowed = opts.forDelete
|
|
170
|
+
? 'only "comment" (log notes) can be deleted'
|
|
171
|
+
: 'only "comment" (log notes) and "Activities" traces can be modified';
|
|
172
|
+
throw usageError(`mail.message #${messageId} has type "${msg.message_type}"${subtypeName ? ` / subtype "${subtypeName}"` : ""} — ${allowed}`, "emails and system notifications are left untouched by design");
|
|
173
|
+
}
|
|
174
|
+
return msg;
|
|
175
|
+
}
|
|
176
|
+
function bodyPreview(body, max = 80) {
|
|
177
|
+
const text = body
|
|
178
|
+
.replace(/<[^>]*>/g, "")
|
|
179
|
+
.replace(/\s+/g, " ")
|
|
180
|
+
.trim();
|
|
181
|
+
return text.length > max ? `${text.substring(0, max)}...` : text;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Update the body of an existing internal log note (mail.message).
|
|
185
|
+
*
|
|
186
|
+
* Use case: repair formatting (escaped HTML, markdown, platform noise) or
|
|
187
|
+
* correct a note posted with wrong content. The message id and date are
|
|
188
|
+
* preserved — only the body changes.
|
|
189
|
+
*/
|
|
190
|
+
export async function updateMessageCommand(messageId, opts, globals, model) {
|
|
191
|
+
const spec = threadModel(model);
|
|
192
|
+
const id = Number.parseInt(messageId, 10);
|
|
193
|
+
if (Number.isNaN(id) || id <= 0) {
|
|
194
|
+
throw usageError(`invalid message id: ${messageId}`, "provide a positive integer");
|
|
195
|
+
}
|
|
196
|
+
const body = await readBody(opts);
|
|
197
|
+
const client = await buildClient(globals);
|
|
198
|
+
const msg = await readLogNote(client, id, spec);
|
|
199
|
+
if (globals.dryRun) {
|
|
200
|
+
const result = {
|
|
201
|
+
dry_run: true,
|
|
202
|
+
message_id: id,
|
|
203
|
+
model: msg.model,
|
|
204
|
+
res_id: msg.res_id,
|
|
205
|
+
old_body_preview: bodyPreview(msg.body),
|
|
206
|
+
new_body_preview: bodyPreview(body),
|
|
207
|
+
new_body_length: body.length,
|
|
208
|
+
};
|
|
209
|
+
if (globals.json) {
|
|
210
|
+
printJson(result);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log(`[DRY RUN] Would update log note #${id} on ${msg.model} #${msg.res_id}:`);
|
|
214
|
+
console.log(` Old: ${result.old_body_preview}`);
|
|
215
|
+
console.log(` New: ${result.new_body_preview}`);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await client.executeKw("mail.message", "write", [[id], { body }]);
|
|
220
|
+
const result = {
|
|
221
|
+
message_id: id,
|
|
222
|
+
model: msg.model,
|
|
223
|
+
res_id: msg.res_id,
|
|
224
|
+
updated: true,
|
|
225
|
+
};
|
|
226
|
+
if (globals.json) {
|
|
227
|
+
printJson(result);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(`✓ Log note #${id} updated on ${msg.model} #${msg.res_id}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Delete internal log notes (mail.message) — e.g. duplicate logs of the
|
|
235
|
+
* same dialog. Unknown ids are skipped with a warning. Only comment-type
|
|
236
|
+
* messages can be deleted.
|
|
237
|
+
*/
|
|
238
|
+
export async function deleteMessageCommand(messageIds, globals, model) {
|
|
239
|
+
const spec = threadModel(model);
|
|
240
|
+
const client = await buildClient(globals);
|
|
241
|
+
const toDelete = [];
|
|
242
|
+
const skipped = [];
|
|
243
|
+
for (const raw of messageIds) {
|
|
244
|
+
const id = Number.parseInt(raw, 10);
|
|
245
|
+
if (Number.isNaN(id) || id <= 0) {
|
|
246
|
+
skipped.push({ id: raw, reason: "invalid id" });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
toDelete.push(await readLogNote(client, id, spec, { forDelete: true }));
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof Error && "exitCode" in error && error.exitCode === 4) {
|
|
254
|
+
skipped.push({ id: raw, reason: "not found" });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (globals.dryRun) {
|
|
261
|
+
const result = {
|
|
262
|
+
dry_run: true,
|
|
263
|
+
would_delete: toDelete.map((m) => ({
|
|
264
|
+
message_id: m.id,
|
|
265
|
+
res_id: m.res_id,
|
|
266
|
+
date: m.date,
|
|
267
|
+
body_preview: bodyPreview(m.body),
|
|
268
|
+
})),
|
|
269
|
+
skipped,
|
|
270
|
+
};
|
|
271
|
+
if (globals.json) {
|
|
272
|
+
printJson(result);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.log(`[DRY RUN] Would delete ${toDelete.length} log note(s):`);
|
|
276
|
+
for (const m of toDelete) {
|
|
277
|
+
console.log(` #${m.id} (${m.date}) ${bodyPreview(m.body)}`);
|
|
278
|
+
}
|
|
279
|
+
for (const s of skipped) {
|
|
280
|
+
console.log(` skipped #${s.id}: ${s.reason}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const deleted = [];
|
|
286
|
+
for (const m of toDelete) {
|
|
287
|
+
await client.executeKw("mail.message", "unlink", [[m.id]]);
|
|
288
|
+
deleted.push(m.id);
|
|
289
|
+
}
|
|
290
|
+
const result = { deleted, skipped };
|
|
291
|
+
if (globals.json) {
|
|
292
|
+
printJson(result);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
console.log(`✓ Deleted ${deleted.length} log note(s): ${deleted.join(", ") || "none"}`);
|
|
296
|
+
for (const s of skipped) {
|
|
297
|
+
console.log(` skipped #${s.id}: ${s.reason}`);
|
|
298
|
+
}
|
|
114
299
|
}
|
|
115
300
|
}
|
|
116
301
|
/**
|