@bergetai/odoo-cli 0.7.2 → 0.8.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 +3 -1
- package/dist/cli.js +17 -5
- package/dist/commands/documents.js +86 -10
- package/dist/commands/notes.js +97 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,9 @@ odoo project tasks list --project "Content Pipeline" --open
|
|
|
40
40
|
odoo project tasks show 850
|
|
41
41
|
odoo documents tree --count
|
|
42
42
|
odoo documents records list --folder "Product Backlog" --limit 20
|
|
43
|
+
odoo documents records list --folder "Projects > Product Backlog" # nested path
|
|
43
44
|
odoo documents records show 25
|
|
45
|
+
odoo documents records download 25 --out ./spec.pdf
|
|
44
46
|
|
|
45
47
|
# Mutations (support --dry-run)
|
|
46
48
|
odoo crm leads create --name "New lead" --email info@example.com
|
|
@@ -78,7 +80,7 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
|
|
|
78
80
|
- Read-only Helpdesk surface (`helpdesk tickets list/show`) — `--open`/`--closed` via stage `fold`, strict team-scoped stage resolution, module-not-installed error (exit 4)
|
|
79
81
|
- Read-only Project surface (`project list/show`) — `--user` (project manager) and `--tag` filters, archived projects excluded by default, module-not-installed error (exit 4)
|
|
80
82
|
- Read-only Project task surface (`project tasks list/show`) — `--project` (required on list, resolved by name or id), project-scoped `--stage`, `--assignee` (many2many `user_ids`), `--tag`, `--priority` (0/Low, 1/High), `--open`/`--closed` via the task's `is_closed` flag, `--domain`/`--filter`, table/JSON output with `search_count` envelope, module-not-installed error (exit 4)
|
|
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`,
|
|
83
|
+
- 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`, `upload` (local file → base64 `datas`, MIME guessed from extension, `--dry-run` previews without sending bytes), and `records download <id>` (binary document → local file, `--out <path>`/`--force`, `--out -` streams to stdout). Everywhere a `--folder` is accepted, folders resolve by name, numeric id, or nested `'Parent > Child'` path (segments resolved within their parent, which disambiguates repeated folder names). Folders are `documents.document` rows with `type='folder'`; the hierarchy is `folder_id`. Module-not-installed error (exit 4)
|
|
82
84
|
- 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`
|
|
83
85
|
- `--dry-run` for all mutations — full validation, resolution, and before/after diffs without mutating
|
|
84
86
|
- 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)
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ticketsCreateCommand, ticketsDeleteCommand, ticketsListCommand, tickets
|
|
|
7
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
|
-
import { documentsDocuments, documentsListCommand, foldersListCommand, documentsShowCommand, documentsCreateCommand, documentsUpdateCommand, documentsDeleteCommand, treeCommand, uploadCommand, } from "./commands/documents.js";
|
|
10
|
+
import { documentsDocuments, documentsListCommand, foldersListCommand, documentsShowCommand, documentsCreateCommand, documentsUpdateCommand, documentsDeleteCommand, treeCommand, uploadCommand, downloadCommand, } from "./commands/documents.js";
|
|
11
11
|
import { versionCommand } from "./commands/version.js";
|
|
12
12
|
import { partnersCreateCommand, partnersDeleteCommand, partnersListCommand, partnersShowCommand, partnersUpdateCommand, resPartner, } from "./commands/partners.js";
|
|
13
13
|
import { CliError } from "./errors.js";
|
|
@@ -140,6 +140,7 @@ Examples:
|
|
|
140
140
|
.option("--body <text>", "note body (HTML allowed)")
|
|
141
141
|
.option("--body-file <path>", "read note body from file ('-' for stdin)")
|
|
142
142
|
.option("--author <email|name|id>", "note author (res.partner; default: your own partner)")
|
|
143
|
+
.option("--attach <path>", "attach a file to the note; repeatable", collect, [])
|
|
143
144
|
.addHelpText("after", `
|
|
144
145
|
IMPORTANT: This command creates INTERNAL notes only. These notes:
|
|
145
146
|
• Are NEVER sent to the customer via email
|
|
@@ -153,7 +154,8 @@ Examples:
|
|
|
153
154
|
$ odoo crm records add-note 338 --body "Internal note text"
|
|
154
155
|
$ odoo crm records add-note 338 --body-file note.md
|
|
155
156
|
$ 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
|
|
157
|
+
$ odoo crm records add-note 338 --body "Kundens svar: ..." --author andreas@berget.ai
|
|
158
|
+
$ odoo crm records add-note 338 --body "Mötesanteckningar" --attach whiteboard.png --attach offert.pdf`)
|
|
157
159
|
.action(async (id, opts) => {
|
|
158
160
|
await run((g) => addNoteCommand(id, opts, g, "crm.lead"), program);
|
|
159
161
|
});
|
|
@@ -394,6 +396,7 @@ the raw form.`)
|
|
|
394
396
|
.option("--body <text>", "note body (HTML allowed)")
|
|
395
397
|
.option("--body-file <path>", "read note body from file ('-' for stdin)")
|
|
396
398
|
.option("--author <email|name|id>", "note author (res.partner; default: your own partner)")
|
|
399
|
+
.option("--attach <path>", "attach a file to the note; repeatable", collect, [])
|
|
397
400
|
.addHelpText("after", `
|
|
398
401
|
IMPORTANT: This command creates INTERNAL notes only. These notes:
|
|
399
402
|
• Are NEVER sent to the customer via email
|
|
@@ -573,7 +576,7 @@ Examples:
|
|
|
573
576
|
.command("list")
|
|
574
577
|
.description("search and list documents (folders excluded by default)")
|
|
575
578
|
.option("--type <type>", "record type: document (default), folder, or all")
|
|
576
|
-
.option("--folder <name|id>", "only records directly in this folder (
|
|
579
|
+
.option("--folder <name|id|path>", "only records directly in this folder (name, id, or 'Parent > Child' path)")
|
|
577
580
|
.option("--owner <name|id>", "filter by owner (login, email, or name)")
|
|
578
581
|
.option("--partner <name|id>", "filter by linked contact")
|
|
579
582
|
.option("--tag <name|id>", "filter by tag; repeatable, AND-ed", collect, [])
|
|
@@ -595,6 +598,15 @@ Examples:
|
|
|
595
598
|
.action(async (id, opts) => {
|
|
596
599
|
await run((g) => documentsShowCommand(id, opts, g), program);
|
|
597
600
|
});
|
|
601
|
+
records
|
|
602
|
+
.command("download")
|
|
603
|
+
.description("download a document's file content to disk")
|
|
604
|
+
.argument("<id>", "record id")
|
|
605
|
+
.option("--out <path>", "output path (default: document name in cwd; '-' for stdout)")
|
|
606
|
+
.option("--force", "overwrite an existing output file")
|
|
607
|
+
.action(async (id, opts) => {
|
|
608
|
+
await run((g) => downloadCommand(id, opts, g), program);
|
|
609
|
+
});
|
|
598
610
|
addMutationFlags(records
|
|
599
611
|
.command("create")
|
|
600
612
|
.description("create a folder or URL link (preview with --dry-run; upload files via 'documents upload')"), documentsDocuments).action(async (opts) => {
|
|
@@ -628,7 +640,7 @@ Examples:
|
|
|
628
640
|
documents
|
|
629
641
|
.command("tree")
|
|
630
642
|
.description("print the folder tree with document counts")
|
|
631
|
-
.option("--folder <name|id>", "start from this folder (default: roots)")
|
|
643
|
+
.option("--folder <name|id|path>", "start from this folder (default: roots; name, id, or 'Parent > Child' path)")
|
|
632
644
|
.option("--depth <n>", "max depth", "10")
|
|
633
645
|
.option("--count", "show the number of documents per folder")
|
|
634
646
|
.action(async (opts) => {
|
|
@@ -639,7 +651,7 @@ Examples:
|
|
|
639
651
|
.description("upload a local file as a document (preview with --dry-run)")
|
|
640
652
|
.requiredOption("--file <path>", "local file path ('-' for stdin)")
|
|
641
653
|
.option("--name <name>", "document name (default: file basename)")
|
|
642
|
-
.option("--folder <name|id>", "target folder (
|
|
654
|
+
.option("--folder <name|id|path>", "target folder (name, id, or 'Parent > Child' path)")
|
|
643
655
|
.option("--owner <name|id>", "owner (login, email, or name)")
|
|
644
656
|
.option("--partner <name|id>", "linked contact")
|
|
645
657
|
.option("--tag <name|id>", "tag; repeatable, AND-ed", collect, [])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, extname, resolve } from "node:path";
|
|
4
|
-
import { usageError } from "../errors.js";
|
|
4
|
+
import { notFoundError, usageError } from "../errors.js";
|
|
5
5
|
import { ModelFields, resolveReference } from "../model.js";
|
|
6
6
|
import { createCommand, deleteCommand, updateCommand, } from "../mutation.js";
|
|
7
7
|
import { colorEnabled, printJson } from "../output.js";
|
|
@@ -19,10 +19,10 @@ const documentsMutation = {
|
|
|
19
19
|
requiredOnCreate: true,
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
flags: "--folder <name|id>",
|
|
22
|
+
flags: "--folder <name|id|path>",
|
|
23
23
|
option: "folder",
|
|
24
24
|
field: "folder_id",
|
|
25
|
-
help: "Parent folder",
|
|
25
|
+
help: "Parent folder (name, id, or 'Parent > Child' path)",
|
|
26
26
|
reference: { model: "documents.document", label: "folder" },
|
|
27
27
|
},
|
|
28
28
|
{
|
|
@@ -391,12 +391,88 @@ export async function uploadCommand(opts, globals) {
|
|
|
391
391
|
const line = `Uploaded documents.document ${id} (${name})`;
|
|
392
392
|
process.stdout.write((color ? `\x1b[32m${line}\x1b[0m` : line) + "\n");
|
|
393
393
|
}
|
|
394
|
-
|
|
394
|
+
export async function downloadCommand(idArg, opts, globals) {
|
|
395
|
+
const client = await authenticatedClient(globals);
|
|
396
|
+
await ModelFields.load(client, "documents.document"); // module-installed check
|
|
397
|
+
if (!/^\d+$/.test(idArg)) {
|
|
398
|
+
throw usageError(`invalid document id ${JSON.stringify(idArg)}`, "expected a numeric id (see 'documents records list')");
|
|
399
|
+
}
|
|
400
|
+
const id = Number(idArg);
|
|
401
|
+
const rows = (await client.executeKw("documents.document", "read", [
|
|
402
|
+
[id],
|
|
403
|
+
["name", "type", "datas", "mimetype"],
|
|
404
|
+
]));
|
|
405
|
+
if (rows.length === 0) {
|
|
406
|
+
throw notFoundError("reference_not_found", `no document with id ${id}`);
|
|
407
|
+
}
|
|
408
|
+
const rec = rows[0];
|
|
409
|
+
if (rec.type === "folder") {
|
|
410
|
+
throw usageError(`document ${id} "${rec.name}" is a folder`, "download expects a binary document (folders excluded by default in 'records list')");
|
|
411
|
+
}
|
|
412
|
+
if (rec.type !== "binary" || rec.datas === false || rec.datas === "") {
|
|
413
|
+
throw usageError(`document ${id} "${rec.name}" has no file content (type: ${rec.type})`);
|
|
414
|
+
}
|
|
415
|
+
const buffer = Buffer.from(rec.datas, "base64");
|
|
416
|
+
// '--out -' streams raw bytes to stdout (for piping).
|
|
417
|
+
if (opts.out === "-") {
|
|
418
|
+
process.stdout.write(buffer);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const outPath = resolve(opts.out ?? rec.name);
|
|
422
|
+
try {
|
|
423
|
+
await stat(outPath);
|
|
424
|
+
if (!opts.force) {
|
|
425
|
+
throw usageError(`file already exists: ${outPath}`, "pass --force to overwrite");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
if (err.code !== "ENOENT")
|
|
430
|
+
throw err;
|
|
431
|
+
}
|
|
432
|
+
await writeFile(outPath, buffer);
|
|
433
|
+
if (globals.json) {
|
|
434
|
+
printJson({
|
|
435
|
+
model: "documents.document",
|
|
436
|
+
action: "download",
|
|
437
|
+
id,
|
|
438
|
+
name: rec.name,
|
|
439
|
+
path: outPath,
|
|
440
|
+
file_size: buffer.length,
|
|
441
|
+
mimetype: rec.mimetype,
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const color = colorEnabled(false) && process.stdout.isTTY === true;
|
|
446
|
+
const line = `Downloaded documents.document ${id} → ${outPath} (${buffer.length} bytes)`;
|
|
447
|
+
process.stdout.write((color ? `\x1b[32m${line}\x1b[0m` : line) + "\n");
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Resolve a folder by name, id, or "Parent > Child > ..." path
|
|
451
|
+
* (must be type=folder). Path segments are resolved within their parent,
|
|
452
|
+
* which disambiguates repeated folder names (e.g. per-customer subfolders).
|
|
453
|
+
*/
|
|
395
454
|
async function resolveFolderId(client, input) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
455
|
+
const segments = input
|
|
456
|
+
.split(">")
|
|
457
|
+
.map((s) => s.trim())
|
|
458
|
+
.filter((s) => s.length > 0);
|
|
459
|
+
if (segments.length <= 1) {
|
|
460
|
+
const resolved = await resolveReference(client, "documents.document", input, {
|
|
461
|
+
label: "folder",
|
|
462
|
+
domain: FOLDER_DOMAIN,
|
|
463
|
+
});
|
|
464
|
+
return resolved.id;
|
|
465
|
+
}
|
|
466
|
+
let parentId = false;
|
|
467
|
+
let id = 0;
|
|
468
|
+
for (const segment of segments) {
|
|
469
|
+
const resolved = await resolveReference(client, "documents.document", segment, {
|
|
470
|
+
label: parentId === false ? "root folder" : `folder under ${parentId}`,
|
|
471
|
+
domain: [...FOLDER_DOMAIN, ["folder_id", "=", parentId]],
|
|
472
|
+
});
|
|
473
|
+
id = resolved.id;
|
|
474
|
+
parentId = id;
|
|
475
|
+
}
|
|
476
|
+
return id;
|
|
401
477
|
}
|
|
402
478
|
export { formatTable, humanCell };
|
package/dist/commands/notes.js
CHANGED
|
@@ -24,6 +24,57 @@ async function buildClient(globals) {
|
|
|
24
24
|
const timeoutMs = parseDurationMs(globals.timeout);
|
|
25
25
|
return new OdooClient(config.url, config.db, config.username, config.apiKey, { timeoutMs, verbose: globals.verbose });
|
|
26
26
|
}
|
|
27
|
+
const MIME_BY_EXT = {
|
|
28
|
+
".txt": "text/plain",
|
|
29
|
+
".md": "text/markdown",
|
|
30
|
+
".csv": "text/csv",
|
|
31
|
+
".json": "application/json",
|
|
32
|
+
".pdf": "application/pdf",
|
|
33
|
+
".png": "image/png",
|
|
34
|
+
".jpg": "image/jpeg",
|
|
35
|
+
".jpeg": "image/jpeg",
|
|
36
|
+
".gif": "image/gif",
|
|
37
|
+
".webp": "image/webp",
|
|
38
|
+
".svg": "image/svg+xml",
|
|
39
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
40
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
41
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
42
|
+
".doc": "application/msword",
|
|
43
|
+
".xls": "application/vnd.ms-excel",
|
|
44
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
45
|
+
".zip": "application/zip",
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Read files for --attach and prepare ir.attachment values. Attachments are
|
|
49
|
+
* linked to the note's mail.message (res_model="mail.message", res_id=note id)
|
|
50
|
+
* so they render on the message in the chatter — same as uploading via the UI.
|
|
51
|
+
*/
|
|
52
|
+
async function prepareAttachments(paths) {
|
|
53
|
+
const { readFile, stat } = await import("node:fs/promises");
|
|
54
|
+
const { basename, extname } = await import("node:path");
|
|
55
|
+
const prepared = [];
|
|
56
|
+
for (const p of paths) {
|
|
57
|
+
let fileStat;
|
|
58
|
+
try {
|
|
59
|
+
fileStat = await stat(p);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw usageError(`attachment file not found: ${p}`, "check the --attach path");
|
|
63
|
+
}
|
|
64
|
+
if (!fileStat.isFile()) {
|
|
65
|
+
throw usageError(`not a regular file: ${p}`);
|
|
66
|
+
}
|
|
67
|
+
const buffer = await readFile(p);
|
|
68
|
+
prepared.push({
|
|
69
|
+
path: p,
|
|
70
|
+
name: basename(p),
|
|
71
|
+
mimetype: MIME_BY_EXT[extname(p).toLowerCase()] ?? "application/octet-stream",
|
|
72
|
+
size: fileStat.size,
|
|
73
|
+
datas: buffer.toString("base64"),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return prepared;
|
|
77
|
+
}
|
|
27
78
|
async function readBody(opts) {
|
|
28
79
|
let body = opts.body;
|
|
29
80
|
if (opts.bodyFile) {
|
|
@@ -50,6 +101,9 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
50
101
|
throw usageError(`invalid ${spec.label} id: ${recordId}`, "provide a positive integer");
|
|
51
102
|
}
|
|
52
103
|
const body = await readBody(opts);
|
|
104
|
+
const attachments = opts.attach?.length
|
|
105
|
+
? await prepareAttachments(opts.attach)
|
|
106
|
+
: [];
|
|
53
107
|
const client = await buildClient(globals);
|
|
54
108
|
// Verify the record exists
|
|
55
109
|
const rows = (await client.executeKw(spec.model, "search_read", [[["id", "=", id]]], { fields: ["id", "name"], limit: 1 }));
|
|
@@ -100,6 +154,11 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
100
154
|
author: author ? { id: author.id, name: author.name } : null,
|
|
101
155
|
body_preview: body.substring(0, 100) + (body.length > 100 ? "..." : ""),
|
|
102
156
|
body_length: body.length,
|
|
157
|
+
attachments: attachments.map((a) => ({
|
|
158
|
+
name: a.name,
|
|
159
|
+
mimetype: a.mimetype,
|
|
160
|
+
size: a.size,
|
|
161
|
+
})),
|
|
103
162
|
};
|
|
104
163
|
if (globals.json) {
|
|
105
164
|
printJson(result);
|
|
@@ -113,12 +172,45 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
113
172
|
}
|
|
114
173
|
console.log(` Body length: ${body.length} characters`);
|
|
115
174
|
console.log(` Body preview: ${result.body_preview}`);
|
|
175
|
+
for (const a of attachments) {
|
|
176
|
+
console.log(` Attachment: ${a.name} (${a.mimetype}, ${a.size} bytes)`);
|
|
177
|
+
}
|
|
116
178
|
}
|
|
117
179
|
return;
|
|
118
180
|
}
|
|
119
|
-
|
|
181
|
+
// NOTE: batch create returns a list of ids — unwrap the single id,
|
|
182
|
+
// otherwise res_id on attachments becomes a nested list and the
|
|
183
|
+
// server faults with "unhashable type: 'list'".
|
|
184
|
+
const noteIdRaw = (await client.executeKw("mail.message", "create", [
|
|
120
185
|
[noteParams],
|
|
121
186
|
]));
|
|
187
|
+
const noteId = Array.isArray(noteIdRaw) ? noteIdRaw[0] : noteIdRaw;
|
|
188
|
+
// Upload attachments, linked to the note's message so they render on it
|
|
189
|
+
// in the chatter (same as attaching via the Odoo UI).
|
|
190
|
+
const createdAttachments = [];
|
|
191
|
+
for (const a of attachments) {
|
|
192
|
+
const attachmentIdRaw = (await client.executeKw("ir.attachment", "create", [
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
name: a.name,
|
|
196
|
+
mimetype: a.mimetype,
|
|
197
|
+
datas: a.datas,
|
|
198
|
+
res_model: "mail.message",
|
|
199
|
+
res_id: noteId,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
]));
|
|
203
|
+
const attachmentId = Array.isArray(attachmentIdRaw)
|
|
204
|
+
? attachmentIdRaw[0]
|
|
205
|
+
: attachmentIdRaw;
|
|
206
|
+
// Link the attachment into the message's attachment_ids m2m — without
|
|
207
|
+
// this the file exists but never renders on the message in the chatter.
|
|
208
|
+
await client.executeKw("mail.message", "write", [
|
|
209
|
+
[noteId],
|
|
210
|
+
{ attachment_ids: [[4, attachmentId]] },
|
|
211
|
+
]);
|
|
212
|
+
createdAttachments.push({ id: attachmentId, name: a.name });
|
|
213
|
+
}
|
|
122
214
|
const result = {
|
|
123
215
|
note_id: noteId,
|
|
124
216
|
model: spec.model,
|
|
@@ -127,6 +219,7 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
127
219
|
note_type: "internal_log_note",
|
|
128
220
|
email_sent: false,
|
|
129
221
|
author: author ? { id: author.id, name: author.name } : null,
|
|
222
|
+
attachments: createdAttachments,
|
|
130
223
|
};
|
|
131
224
|
if (globals.json) {
|
|
132
225
|
printJson(result);
|
|
@@ -138,6 +231,9 @@ export async function addNoteCommand(recordId, opts, globals, model) {
|
|
|
138
231
|
if (author) {
|
|
139
232
|
console.log(` Author: ${author.name} (${author.id})`);
|
|
140
233
|
}
|
|
234
|
+
for (const a of createdAttachments) {
|
|
235
|
+
console.log(` Attachment: ${a.name} (ID: ${a.id})`);
|
|
236
|
+
}
|
|
141
237
|
}
|
|
142
238
|
}
|
|
143
239
|
/**
|