@bergetai/odoo-cli 0.2.0 → 0.3.2
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 +13 -0
- package/dist/cli.js +115 -1
- package/dist/commands/crm.js +17 -0
- package/dist/commands/documents.js +402 -0
- package/dist/commands/project.js +221 -2
- package/dist/mutation.js +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,11 @@ odoo crm leads list --open --limit 20
|
|
|
29
29
|
odoo crm leads show 42
|
|
30
30
|
odoo helpdesk tickets list --team Support --json
|
|
31
31
|
odoo project list --json
|
|
32
|
+
odoo project tasks list --project "Content Pipeline" --open
|
|
33
|
+
odoo project tasks show 850
|
|
34
|
+
odoo documents tree --count
|
|
35
|
+
odoo documents records list --folder "Product Backlog" --limit 20
|
|
36
|
+
odoo documents records show 25
|
|
32
37
|
|
|
33
38
|
# Mutations (support --dry-run)
|
|
34
39
|
odoo crm leads create --name "New lead" --email info@example.com
|
|
@@ -49,6 +54,8 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
|
|
|
49
54
|
- Read-only CRM surface (`crm leads list/show`) — default columns, `--fields`/`--all-fields`, `--limit`/`--offset`/`--order`, `--domain`/`--filter` with `fields_get` type checking, reference resolution (`--stage`/`--team`/`--assignee`/`--tag`), table/JSON output with `search_count` envelope
|
|
50
55
|
- Read-only Helpdesk surface (`helpdesk tickets list/show`) — `--open`/`--closed` via stage `fold`, strict team-scoped stage resolution, module-not-installed error (exit 4)
|
|
51
56
|
- Read-only Project surface (`project list/show`) — `--user` (project manager) and `--tag` filters, archived projects excluded by default, module-not-installed error (exit 4)
|
|
57
|
+
- 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)
|
|
58
|
+
- 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)
|
|
52
59
|
- 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`
|
|
53
60
|
- `--dry-run` for all mutations — full validation, resolution, and before/after diffs without mutating
|
|
54
61
|
- 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
|
|
@@ -68,6 +75,12 @@ against the live server:
|
|
|
68
75
|
- `execute_kw` kwargs must be a separate trailing dict param; packing it into
|
|
69
76
|
the positional-args array makes the server silently ignore it (this caused
|
|
70
77
|
`fields_get` to return full/empty structs).
|
|
78
|
+
- `project.task` has no `user_id` field; assignees are the many2many
|
|
79
|
+
`user_ids`. The `priority` selection is two-valued (0/Low, 1/High), not the
|
|
80
|
+
four-level scale used by CRM/Helpdesk. Tasks carry a direct `is_closed`
|
|
81
|
+
boolean, which `--open`/`--closed` filter on (authoritative, unlike stage
|
|
82
|
+
`fold`). Stages (`project.task.type`) link to projects via the many2many
|
|
83
|
+
`project_ids`, so `--stage` resolves within the `--project`.
|
|
71
84
|
|
|
72
85
|
## Development
|
|
73
86
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,8 @@ import { authLoginCommand, authStatusCommand } from "./commands/auth.js";
|
|
|
4
4
|
import { leadsCreateCommand, leadsDeleteCommand, leadsListCommand, leadsShowCommand, leadsUpdateCommand, crmLeads, } from "./commands/crm.js";
|
|
5
5
|
import { ticketsCreateCommand, ticketsDeleteCommand, ticketsListCommand, ticketsShowCommand, ticketsUpdateCommand, helpdeskTickets, } from "./commands/helpdesk.js";
|
|
6
6
|
import { addNoteCommand, listMessagesCommand, } from "./commands/notes.js";
|
|
7
|
-
import { createTaskCommand, projectListCommand, projectShowCommand, } from "./commands/project.js";
|
|
7
|
+
import { createTaskCommand, projectListCommand, projectShowCommand, tasksListCommand, tasksShowCommand, } from "./commands/project.js";
|
|
8
|
+
import { documentsDocuments, documentsListCommand, foldersListCommand, documentsShowCommand, documentsCreateCommand, documentsUpdateCommand, documentsDeleteCommand, treeCommand, uploadCommand, } from "./commands/documents.js";
|
|
8
9
|
import { versionCommand } from "./commands/version.js";
|
|
9
10
|
import { CliError } from "./errors.js";
|
|
10
11
|
import { applyGlobalOptions, enforceTransportSecurity, DEFAULT_TIMEOUT, } from "./global-options.js";
|
|
@@ -263,6 +264,34 @@ Examples:
|
|
|
263
264
|
await run((g) => projectShowCommand(id, opts, g), program);
|
|
264
265
|
});
|
|
265
266
|
const tasks = project.command("tasks").description("project tasks");
|
|
267
|
+
tasks
|
|
268
|
+
.command("list")
|
|
269
|
+
.description("search and list tasks in a project with curated columns")
|
|
270
|
+
.requiredOption("--project <name|id>", "project name or ID")
|
|
271
|
+
.option("--stage <name|id>", "filter by stage (resolved within the project)")
|
|
272
|
+
.option("--assignee <name|id>", "filter by assignee (login, email, or name)")
|
|
273
|
+
.option("--tag <name|id>", "filter by tag; repeatable, AND-ed", collect, [])
|
|
274
|
+
.option("--priority <level>", "filter by priority: 0 (low/normal) or 1 (high)")
|
|
275
|
+
.option("--open", "only open tasks (is_closed = false)")
|
|
276
|
+
.option("--closed", "only closed tasks (is_closed = true)")
|
|
277
|
+
.option("--filter <f=v>", "equality filter; repeatable, AND-ed", collect, [])
|
|
278
|
+
.option("--domain <json>", "Odoo domain as a JSON array (mutually exclusive with --filter)")
|
|
279
|
+
.option("--fields <a,b,c>", "columns to display")
|
|
280
|
+
.option("--limit <n>", "max records (default: 50)", "50")
|
|
281
|
+
.option("--offset <n>", "skip the first n records", "0")
|
|
282
|
+
.option("--order <order>", 'sort order, e.g. "priority desc"', undefined)
|
|
283
|
+
.action(async (opts) => {
|
|
284
|
+
await run((g) => tasksListCommand(opts, g), program);
|
|
285
|
+
});
|
|
286
|
+
tasks
|
|
287
|
+
.command("show")
|
|
288
|
+
.description("read one task")
|
|
289
|
+
.argument("<id>", "task id")
|
|
290
|
+
.option("--fields <a,b,c>", "fields to display")
|
|
291
|
+
.option("--all-fields", "display every readable field")
|
|
292
|
+
.action(async (id, opts) => {
|
|
293
|
+
await run((g) => tasksShowCommand(id, opts, g), program);
|
|
294
|
+
});
|
|
266
295
|
tasks
|
|
267
296
|
.command("create")
|
|
268
297
|
.description("create a task in a project (preview with --dry-run)")
|
|
@@ -300,6 +329,91 @@ Examples:
|
|
|
300
329
|
.action(async (opts) => {
|
|
301
330
|
await run((g) => createTaskCommand(opts, g), program);
|
|
302
331
|
});
|
|
332
|
+
const documents = program
|
|
333
|
+
.command("documents")
|
|
334
|
+
.description("Documents app (documents.document, Enterprise)");
|
|
335
|
+
const records = documents
|
|
336
|
+
.command("records")
|
|
337
|
+
.description("files, links and folders");
|
|
338
|
+
records
|
|
339
|
+
.command("list")
|
|
340
|
+
.description("search and list documents (folders excluded by default)")
|
|
341
|
+
.option("--type <type>", "record type: document (default), folder, or all")
|
|
342
|
+
.option("--folder <name|id>", "only records directly in this folder (resolved by name)")
|
|
343
|
+
.option("--owner <name|id>", "filter by owner (login, email, or name)")
|
|
344
|
+
.option("--partner <name|id>", "filter by linked contact")
|
|
345
|
+
.option("--tag <name|id>", "filter by tag; repeatable, AND-ed", collect, [])
|
|
346
|
+
.option("--filter <f=v>", "equality filter; repeatable, AND-ed", collect, [])
|
|
347
|
+
.option("--domain <json>", "Odoo domain as a JSON array (mutually exclusive with --filter)")
|
|
348
|
+
.option("--fields <a,b,c>", "columns to display")
|
|
349
|
+
.option("--limit <n>", "max records (default: 50)", "50")
|
|
350
|
+
.option("--offset <n>", "skip the first n records", "0")
|
|
351
|
+
.option("--order <order>", 'sort order, e.g. "create_date desc"', undefined)
|
|
352
|
+
.action(async (opts) => {
|
|
353
|
+
await run((g) => documentsListCommand(opts, g), program);
|
|
354
|
+
});
|
|
355
|
+
records
|
|
356
|
+
.command("show")
|
|
357
|
+
.description("read one document or folder")
|
|
358
|
+
.argument("<id>", "record id")
|
|
359
|
+
.option("--fields <a,b,c>", "fields to display")
|
|
360
|
+
.option("--all-fields", "display every readable field")
|
|
361
|
+
.action(async (id, opts) => {
|
|
362
|
+
await run((g) => documentsShowCommand(id, opts, g), program);
|
|
363
|
+
});
|
|
364
|
+
addMutationFlags(records
|
|
365
|
+
.command("create")
|
|
366
|
+
.description("create a folder or URL link (preview with --dry-run; upload files via 'documents upload')"), documentsDocuments).action(async (opts) => {
|
|
367
|
+
await run((g) => documentsCreateCommand(opts, g), program);
|
|
368
|
+
});
|
|
369
|
+
addMutationFlags(records
|
|
370
|
+
.command("update")
|
|
371
|
+
.description("update documents (preview with --dry-run)"), documentsDocuments)
|
|
372
|
+
.argument("<id...>", "record ids")
|
|
373
|
+
.action(async (ids, opts) => {
|
|
374
|
+
await run((g) => documentsUpdateCommand(ids, opts, g), program);
|
|
375
|
+
});
|
|
376
|
+
records
|
|
377
|
+
.command("delete")
|
|
378
|
+
.description("delete documents (preview with --dry-run; unknown ids are skipped)")
|
|
379
|
+
.argument("<id...>", "record ids")
|
|
380
|
+
.action(async (ids) => {
|
|
381
|
+
await run((g) => documentsDeleteCommand(ids, g), program);
|
|
382
|
+
});
|
|
383
|
+
const folders = documents.command("folders").description("folders");
|
|
384
|
+
folders
|
|
385
|
+
.command("list")
|
|
386
|
+
.description("list all folders")
|
|
387
|
+
.option("--fields <a,b,c>", "columns to display")
|
|
388
|
+
.option("--limit <n>", "max records (default: 50)", "50")
|
|
389
|
+
.option("--offset <n>", "skip the first n records", "0")
|
|
390
|
+
.option("--order <order>", 'sort order, e.g. "name asc"', undefined)
|
|
391
|
+
.action(async (opts) => {
|
|
392
|
+
await run((g) => foldersListCommand(opts, g), program);
|
|
393
|
+
});
|
|
394
|
+
documents
|
|
395
|
+
.command("tree")
|
|
396
|
+
.description("print the folder tree with document counts")
|
|
397
|
+
.option("--folder <name|id>", "start from this folder (default: roots)")
|
|
398
|
+
.option("--depth <n>", "max depth", "10")
|
|
399
|
+
.option("--count", "show the number of documents per folder")
|
|
400
|
+
.action(async (opts) => {
|
|
401
|
+
await run((g) => treeCommand(opts, g), program);
|
|
402
|
+
});
|
|
403
|
+
documents
|
|
404
|
+
.command("upload")
|
|
405
|
+
.description("upload a local file as a document (preview with --dry-run)")
|
|
406
|
+
.requiredOption("--file <path>", "local file path ('-' for stdin)")
|
|
407
|
+
.option("--name <name>", "document name (default: file basename)")
|
|
408
|
+
.option("--folder <name|id>", "target folder (resolved by name)")
|
|
409
|
+
.option("--owner <name|id>", "owner (login, email, or name)")
|
|
410
|
+
.option("--partner <name|id>", "linked contact")
|
|
411
|
+
.option("--tag <name|id>", "tag; repeatable, AND-ed", collect, [])
|
|
412
|
+
.option("--description <text>", "description")
|
|
413
|
+
.option("--mimetype <type>", "MIME type (default: guessed from extension)")
|
|
414
|
+
.action(async (opts) => {
|
|
415
|
+
await run((g) => uploadCommand(opts, g), program);
|
|
416
|
+
});
|
|
303
417
|
return program;
|
|
304
418
|
}
|
|
305
419
|
async function run(fn, program) {
|
package/dist/commands/crm.js
CHANGED
|
@@ -134,7 +134,24 @@ const crmLeadsMutation = {
|
|
|
134
134
|
field: "description",
|
|
135
135
|
help: "Notes",
|
|
136
136
|
},
|
|
137
|
+
{
|
|
138
|
+
flags: "--lead-type <type>",
|
|
139
|
+
option: "leadType",
|
|
140
|
+
field: "x_studio_lead_type",
|
|
141
|
+
help: "Lead type: Inbound|Outbound|Not sure (default: Inbound)",
|
|
142
|
+
parse: (raw, flagName) => {
|
|
143
|
+
const v = raw.trim();
|
|
144
|
+
const valid = ["Inbound", "Outbound", "Not sure"];
|
|
145
|
+
if (!valid.includes(v)) {
|
|
146
|
+
throw usageError(`invalid ${flagName} ${JSON.stringify(raw)}`, `expected one of: ${valid.join(", ")}`);
|
|
147
|
+
}
|
|
148
|
+
return v;
|
|
149
|
+
},
|
|
150
|
+
},
|
|
137
151
|
],
|
|
152
|
+
createDefaultValues: {
|
|
153
|
+
x_studio_lead_type: "Inbound",
|
|
154
|
+
},
|
|
138
155
|
};
|
|
139
156
|
export const crmLeads = {
|
|
140
157
|
model: "crm.lead",
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, resolve } from "node:path";
|
|
4
|
+
import { usageError } from "../errors.js";
|
|
5
|
+
import { ModelFields, resolveReference } from "../model.js";
|
|
6
|
+
import { createCommand, deleteCommand, updateCommand, } from "../mutation.js";
|
|
7
|
+
import { colorEnabled, printJson } from "../output.js";
|
|
8
|
+
import { authenticatedClient, listCommand, showCommand, } from "../resource.js";
|
|
9
|
+
import { formatTable, humanCell } from "../table.js";
|
|
10
|
+
const FOLDER_DOMAIN = [["type", "=", "folder"]];
|
|
11
|
+
/** documents.document mutation flags (folders + binary files share the model). */
|
|
12
|
+
const documentsMutation = {
|
|
13
|
+
flags: [
|
|
14
|
+
{
|
|
15
|
+
flags: "--name <name>",
|
|
16
|
+
option: "name",
|
|
17
|
+
field: "name",
|
|
18
|
+
help: "Document or folder name",
|
|
19
|
+
requiredOnCreate: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
flags: "--folder <name|id>",
|
|
23
|
+
option: "folder",
|
|
24
|
+
field: "folder_id",
|
|
25
|
+
help: "Parent folder",
|
|
26
|
+
reference: { model: "documents.document", label: "folder" },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
flags: "--owner <name|id>",
|
|
30
|
+
option: "owner",
|
|
31
|
+
field: "owner_id",
|
|
32
|
+
help: "Owner (login, email, or name)",
|
|
33
|
+
reference: {
|
|
34
|
+
model: "res.users",
|
|
35
|
+
label: "user",
|
|
36
|
+
searchFields: ["login", "email", "name"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
flags: "--partner <name|id>",
|
|
41
|
+
option: "partner",
|
|
42
|
+
field: "partner_id",
|
|
43
|
+
help: "Related partner",
|
|
44
|
+
reference: { model: "res.partner", label: "partner" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
flags: "--tag <name|id>",
|
|
48
|
+
option: "tag",
|
|
49
|
+
field: "tag_ids",
|
|
50
|
+
help: "Tag; repeatable, replaces all tags",
|
|
51
|
+
multi: true,
|
|
52
|
+
reference: { model: "documents.tag", label: "tag" },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
flags: "--description <text>",
|
|
56
|
+
option: "description",
|
|
57
|
+
field: "description",
|
|
58
|
+
help: "Description",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
flags: "--link <url>",
|
|
62
|
+
option: "link",
|
|
63
|
+
field: "url",
|
|
64
|
+
help: "External URL (creates a link document)",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
flags: "--folder-type <folder>",
|
|
68
|
+
option: "folderType",
|
|
69
|
+
field: "type",
|
|
70
|
+
help: "Create a folder instead of a document (pass --folder-type folder)",
|
|
71
|
+
parse: (raw, flagName) => {
|
|
72
|
+
if (raw !== "folder") {
|
|
73
|
+
throw usageError(`invalid ${flagName} value ${JSON.stringify(raw)}`, "expected: folder");
|
|
74
|
+
}
|
|
75
|
+
return "folder";
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
export const documentsDocuments = {
|
|
81
|
+
model: "documents.document",
|
|
82
|
+
showCommand: "odoo documents records show",
|
|
83
|
+
defaultListFields: [
|
|
84
|
+
"id",
|
|
85
|
+
"name",
|
|
86
|
+
"type",
|
|
87
|
+
"folder_id",
|
|
88
|
+
"mimetype",
|
|
89
|
+
"file_size",
|
|
90
|
+
"owner_id",
|
|
91
|
+
"create_date",
|
|
92
|
+
],
|
|
93
|
+
showExtraFields: [
|
|
94
|
+
"partner_id",
|
|
95
|
+
"tag_ids",
|
|
96
|
+
"description",
|
|
97
|
+
"url",
|
|
98
|
+
"access_internal",
|
|
99
|
+
"access_via_link",
|
|
100
|
+
"access_url",
|
|
101
|
+
"is_editable_attachment",
|
|
102
|
+
"handler",
|
|
103
|
+
"active",
|
|
104
|
+
"shortcut_document_id",
|
|
105
|
+
"write_date",
|
|
106
|
+
],
|
|
107
|
+
resolutionHints: {
|
|
108
|
+
folder_id: "--folder",
|
|
109
|
+
owner_id: "--owner",
|
|
110
|
+
partner_id: "--partner",
|
|
111
|
+
tag_ids: "--tag",
|
|
112
|
+
},
|
|
113
|
+
priorityLabels: {},
|
|
114
|
+
pinnedDomain: [["type", "!=", "folder"]],
|
|
115
|
+
references: {
|
|
116
|
+
assignee: {
|
|
117
|
+
model: "res.users",
|
|
118
|
+
label: "user",
|
|
119
|
+
field: "owner_id",
|
|
120
|
+
searchFields: ["login", "email", "name"],
|
|
121
|
+
},
|
|
122
|
+
tag: { model: "documents.tag", label: "tag", field: "tag_ids" },
|
|
123
|
+
},
|
|
124
|
+
mutation: documentsMutation,
|
|
125
|
+
extraFilters: async (opts, client) => {
|
|
126
|
+
const o = opts;
|
|
127
|
+
const clauses = [];
|
|
128
|
+
if (o.folder !== undefined) {
|
|
129
|
+
clauses.push(["folder_id", "=", await resolveFolderId(client, o.folder)]);
|
|
130
|
+
}
|
|
131
|
+
if (o.type === "folder") {
|
|
132
|
+
clauses.push(["type", "=", "folder"]);
|
|
133
|
+
}
|
|
134
|
+
else if (o.type === "binary") {
|
|
135
|
+
clauses.push(["type", "=", "binary"]);
|
|
136
|
+
}
|
|
137
|
+
else if (o.type === "url") {
|
|
138
|
+
clauses.push(["type", "=", "url"]);
|
|
139
|
+
}
|
|
140
|
+
else if (o.type === "all") {
|
|
141
|
+
// no extra clause — but pinnedDomain excludes folders, so drop it
|
|
142
|
+
// (handled by caller clearing pinnedDomain)
|
|
143
|
+
}
|
|
144
|
+
return clauses;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* List folders only (type = 'folder'). Same list engine, pinned domain
|
|
149
|
+
* flipped to folders-only.
|
|
150
|
+
*/
|
|
151
|
+
export const documentsFolders = {
|
|
152
|
+
...documentsDocuments,
|
|
153
|
+
showCommand: "odoo documents folders show",
|
|
154
|
+
pinnedDomain: FOLDER_DOMAIN,
|
|
155
|
+
};
|
|
156
|
+
export const documentsAll = {
|
|
157
|
+
...documentsDocuments,
|
|
158
|
+
pinnedDomain: [],
|
|
159
|
+
};
|
|
160
|
+
export function documentsListCommand(opts, globals) {
|
|
161
|
+
const config = opts.type === "all"
|
|
162
|
+
? documentsAll
|
|
163
|
+
: opts.type === "folder"
|
|
164
|
+
? documentsFolders
|
|
165
|
+
: documentsDocuments;
|
|
166
|
+
return listCommand(config, opts, globals);
|
|
167
|
+
}
|
|
168
|
+
export function foldersListCommand(opts, globals) {
|
|
169
|
+
return listCommand(documentsFolders, opts, globals);
|
|
170
|
+
}
|
|
171
|
+
export function documentsShowCommand(idArg, opts, globals) {
|
|
172
|
+
return showCommand(documentsAll, idArg, opts, globals);
|
|
173
|
+
}
|
|
174
|
+
export function documentsCreateCommand(opts, globals) {
|
|
175
|
+
const config = opts.folderType === "folder" ? documentsAll : documentsDocuments;
|
|
176
|
+
return createCommand(config, opts, globals);
|
|
177
|
+
}
|
|
178
|
+
export function documentsUpdateCommand(idArgs, opts, globals) {
|
|
179
|
+
return updateCommand(documentsAll, idArgs, opts, globals);
|
|
180
|
+
}
|
|
181
|
+
export function documentsDeleteCommand(idArgs, globals) {
|
|
182
|
+
return deleteCommand(documentsAll, idArgs, globals);
|
|
183
|
+
}
|
|
184
|
+
export async function treeCommand(opts, globals) {
|
|
185
|
+
const client = await authenticatedClient(globals);
|
|
186
|
+
await ModelFields.load(client, "documents.document"); // module-installed check
|
|
187
|
+
const maxDepth = Number(opts.depth);
|
|
188
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1) {
|
|
189
|
+
throw usageError(`invalid --depth value ${JSON.stringify(opts.depth)}`, "expected a positive integer");
|
|
190
|
+
}
|
|
191
|
+
// Root resolution: --folder given → that folder as root; otherwise roots.
|
|
192
|
+
let rootId = false;
|
|
193
|
+
let rootName = "Documents";
|
|
194
|
+
if (opts.folder !== undefined) {
|
|
195
|
+
rootId = await resolveFolderId(client, opts.folder);
|
|
196
|
+
const rows = (await client.executeKw("documents.document", "read", [
|
|
197
|
+
[rootId],
|
|
198
|
+
["name"],
|
|
199
|
+
]));
|
|
200
|
+
rootName = rows[0]?.name ?? rootName;
|
|
201
|
+
}
|
|
202
|
+
const [folders, docs] = await Promise.all([
|
|
203
|
+
client.executeKw("documents.document", "search_read", [FOLDER_DOMAIN], { fields: ["id", "name", "folder_id"], limit: 5000 }),
|
|
204
|
+
opts.count
|
|
205
|
+
? client.executeKw("documents.document", "search_read", [[["type", "!=", "folder"]]], { fields: ["id", "folder_id"], limit: 50000 })
|
|
206
|
+
: Promise.resolve([]),
|
|
207
|
+
]);
|
|
208
|
+
const countByFolder = new Map();
|
|
209
|
+
for (const d of docs) {
|
|
210
|
+
const fid = Array.isArray(d.folder_id) ? d.folder_id[0] : false;
|
|
211
|
+
if (fid !== false) {
|
|
212
|
+
countByFolder.set(fid, (countByFolder.get(fid) ?? 0) + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const nodes = new Map();
|
|
216
|
+
for (const f of folders) {
|
|
217
|
+
nodes.set(f.id, {
|
|
218
|
+
id: f.id,
|
|
219
|
+
name: f.name,
|
|
220
|
+
parentId: Array.isArray(f.folder_id) ? f.folder_id[0] : false,
|
|
221
|
+
documentCount: countByFolder.get(f.id) ?? 0,
|
|
222
|
+
children: [],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
for (const node of nodes.values()) {
|
|
226
|
+
if (node.parentId !== false) {
|
|
227
|
+
const parent = nodes.get(node.parentId);
|
|
228
|
+
if (parent)
|
|
229
|
+
parent.children.push(node);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Root set: explicit folder, or all folders without a parent in the map.
|
|
233
|
+
const roots = [];
|
|
234
|
+
if (rootId !== false) {
|
|
235
|
+
const root = nodes.get(rootId);
|
|
236
|
+
if (root)
|
|
237
|
+
roots.push(root);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
for (const node of nodes.values()) {
|
|
241
|
+
if (node.parentId === false || !nodes.has(node.parentId)) {
|
|
242
|
+
roots.push(node);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
roots.sort((a, b) => a.name.localeCompare(b.name));
|
|
247
|
+
if (globals.json) {
|
|
248
|
+
const toJson = (n) => ({
|
|
249
|
+
id: n.id,
|
|
250
|
+
name: n.name,
|
|
251
|
+
document_count: n.documentCount,
|
|
252
|
+
children: n.children
|
|
253
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
254
|
+
.map(toJson),
|
|
255
|
+
});
|
|
256
|
+
printJson({ root: rootName, tree: roots.map(toJson) });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const lines = [];
|
|
260
|
+
const renderRec = (node, prefix, depth) => {
|
|
261
|
+
const countSuffix = opts.count ? ` (${node.documentCount})` : "";
|
|
262
|
+
lines.push(`${prefix}${node.name}${countSuffix}`);
|
|
263
|
+
if (depth >= maxDepth)
|
|
264
|
+
return;
|
|
265
|
+
const children = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
|
|
266
|
+
for (let i = 0; i < children.length; i++) {
|
|
267
|
+
const last = i === children.length - 1;
|
|
268
|
+
renderRec(children[i], prefix + (last ? "└── " : "├── "), depth + 1);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
for (const root of roots) {
|
|
272
|
+
renderRec(root, "", 1);
|
|
273
|
+
}
|
|
274
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// upload — local file → binary document
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
const MIME_BY_EXT = {
|
|
280
|
+
".txt": "text/plain",
|
|
281
|
+
".md": "text/markdown",
|
|
282
|
+
".csv": "text/csv",
|
|
283
|
+
".json": "application/json",
|
|
284
|
+
".pdf": "application/pdf",
|
|
285
|
+
".png": "image/png",
|
|
286
|
+
".jpg": "image/jpeg",
|
|
287
|
+
".jpeg": "image/jpeg",
|
|
288
|
+
".gif": "image/gif",
|
|
289
|
+
".webp": "image/webp",
|
|
290
|
+
".svg": "image/svg+xml",
|
|
291
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
292
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
293
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
294
|
+
".doc": "application/msword",
|
|
295
|
+
".xls": "application/vnd.ms-excel",
|
|
296
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
297
|
+
".zip": "application/zip",
|
|
298
|
+
".html": "text/html",
|
|
299
|
+
".xml": "application/xml",
|
|
300
|
+
};
|
|
301
|
+
export async function uploadCommand(opts, globals) {
|
|
302
|
+
const client = await authenticatedClient(globals);
|
|
303
|
+
const fieldsMeta = await ModelFields.load(client, "documents.document");
|
|
304
|
+
const filePath = resolve(opts.file);
|
|
305
|
+
let fileStat;
|
|
306
|
+
try {
|
|
307
|
+
fileStat = await stat(filePath);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
throw usageError(`file not found: ${opts.file}`);
|
|
311
|
+
}
|
|
312
|
+
if (!fileStat.isFile()) {
|
|
313
|
+
throw usageError(`not a regular file: ${opts.file}`);
|
|
314
|
+
}
|
|
315
|
+
const name = opts.name ?? basename(filePath);
|
|
316
|
+
const mimetype = opts.mimetype ??
|
|
317
|
+
MIME_BY_EXT[extname(filePath).toLowerCase()] ??
|
|
318
|
+
"application/octet-stream";
|
|
319
|
+
// Read + base64
|
|
320
|
+
const buffer = await readFile(filePath);
|
|
321
|
+
const datas = buffer.toString("base64");
|
|
322
|
+
const checksum = createHash("sha1").update(buffer).digest("hex");
|
|
323
|
+
const values = {
|
|
324
|
+
name,
|
|
325
|
+
mimetype,
|
|
326
|
+
file_size: fileStat.size,
|
|
327
|
+
checksum,
|
|
328
|
+
datas,
|
|
329
|
+
};
|
|
330
|
+
if (opts.folder !== undefined) {
|
|
331
|
+
values.folder_id = await resolveFolderId(client, opts.folder);
|
|
332
|
+
}
|
|
333
|
+
if (opts.owner !== undefined) {
|
|
334
|
+
const user = await resolveReference(client, "res.users", opts.owner, {
|
|
335
|
+
label: "user",
|
|
336
|
+
searchFields: ["login", "email", "name"],
|
|
337
|
+
});
|
|
338
|
+
values.owner_id = user.id;
|
|
339
|
+
}
|
|
340
|
+
if (opts.partner !== undefined) {
|
|
341
|
+
const partner = await resolveReference(client, "res.partner", opts.partner, { label: "partner" });
|
|
342
|
+
values.partner_id = partner.id;
|
|
343
|
+
}
|
|
344
|
+
if (opts.tag && opts.tag.length > 0) {
|
|
345
|
+
const ids = [];
|
|
346
|
+
for (const t of opts.tag) {
|
|
347
|
+
const tag = await resolveReference(client, "documents.tag", t, {
|
|
348
|
+
label: "tag",
|
|
349
|
+
});
|
|
350
|
+
ids.push(tag.id);
|
|
351
|
+
}
|
|
352
|
+
values.tag_ids = [[6, 0, ids]];
|
|
353
|
+
}
|
|
354
|
+
if (opts.description !== undefined) {
|
|
355
|
+
values.description = opts.description;
|
|
356
|
+
}
|
|
357
|
+
if (globals.dryRun) {
|
|
358
|
+
const preview = { ...values, datas: `<${buffer.length} bytes base64>` };
|
|
359
|
+
if (globals.json) {
|
|
360
|
+
printJson({
|
|
361
|
+
model: "documents.document",
|
|
362
|
+
action: "upload",
|
|
363
|
+
dry_run: true,
|
|
364
|
+
values: preview,
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
process.stdout.write(`Would upload documents.document:\n` +
|
|
369
|
+
Object.entries(preview)
|
|
370
|
+
.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`)
|
|
371
|
+
.join("\n") +
|
|
372
|
+
"\n");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const id = (await client.executeKw("documents.document", "create", [
|
|
376
|
+
values,
|
|
377
|
+
]));
|
|
378
|
+
if (globals.json) {
|
|
379
|
+
printJson({
|
|
380
|
+
model: "documents.document",
|
|
381
|
+
action: "upload",
|
|
382
|
+
dry_run: false,
|
|
383
|
+
id,
|
|
384
|
+
name,
|
|
385
|
+
mimetype,
|
|
386
|
+
file_size: fileStat.size,
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const color = colorEnabled(false) && process.stdout.isTTY === true;
|
|
391
|
+
const line = `Uploaded documents.document ${id} (${name})`;
|
|
392
|
+
process.stdout.write((color ? `\x1b[32m${line}\x1b[0m` : line) + "\n");
|
|
393
|
+
}
|
|
394
|
+
/** Resolve a folder by name or id (must be type=folder). */
|
|
395
|
+
async function resolveFolderId(client, input) {
|
|
396
|
+
const resolved = await resolveReference(client, "documents.document", input, {
|
|
397
|
+
label: "folder",
|
|
398
|
+
domain: FOLDER_DOMAIN,
|
|
399
|
+
});
|
|
400
|
+
return resolved.id;
|
|
401
|
+
}
|
|
402
|
+
export { formatTable, humanCell };
|
package/dist/commands/project.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { OdooClient } from "../client.js";
|
|
2
2
|
import { resolveConfig } from "../config.js";
|
|
3
3
|
import { notFoundError, usageError } from "../errors.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { buildFilterClauses, parseDomain } from "../filter.js";
|
|
5
|
+
import { ModelFields, resolveReference } from "../model.js";
|
|
6
|
+
import { colorEnabled, printJson, verbose as logVerbose } from "../output.js";
|
|
7
|
+
import { authenticatedClient, listCommand, parseId, showCommand, } from "../resource.js";
|
|
8
|
+
import { formatKeyValues, formatTable, humanCell } from "../table.js";
|
|
6
9
|
/**
|
|
7
10
|
* Create a task in an Odoo project.
|
|
8
11
|
*
|
|
@@ -188,3 +191,219 @@ export async function createTaskCommand(opts, globals) {
|
|
|
188
191
|
console.log(` Tags: ${tagIds.join(", ")}`);
|
|
189
192
|
}
|
|
190
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* project.task diverges from the shared list/show engine (resource.ts) in
|
|
196
|
+
* three ways that make a custom command cleaner than engine surgery:
|
|
197
|
+
* - assignee is user_ids (many2many), not user_id (many2one)
|
|
198
|
+
* - priority is a 2-value selection (0/Low, 1/High), not the engine's
|
|
199
|
+
* hardcoded low/medium/high/urgent map
|
|
200
|
+
* - stages scope via project_ids (many2many) on project.task.type, not
|
|
201
|
+
* team_id/team_ids
|
|
202
|
+
* The resolution/ordering mirrors listCommand (resource.ts) and the project
|
|
203
|
+
* resolution mirrors createTaskCommand above.
|
|
204
|
+
*/
|
|
205
|
+
const TASK_DEFAULT_LIST_FIELDS = [
|
|
206
|
+
"id",
|
|
207
|
+
"name",
|
|
208
|
+
"stage_id",
|
|
209
|
+
"user_ids",
|
|
210
|
+
"priority",
|
|
211
|
+
"date_deadline",
|
|
212
|
+
"is_closed",
|
|
213
|
+
];
|
|
214
|
+
const TASK_SHOW_EXTRA_FIELDS = [
|
|
215
|
+
"description",
|
|
216
|
+
"partner_id",
|
|
217
|
+
"tag_ids",
|
|
218
|
+
"parent_id",
|
|
219
|
+
"milestone_id",
|
|
220
|
+
"active",
|
|
221
|
+
"create_date",
|
|
222
|
+
"write_date",
|
|
223
|
+
];
|
|
224
|
+
const TASK_PRIORITY_LABELS = {
|
|
225
|
+
"0": "Low",
|
|
226
|
+
"1": "High",
|
|
227
|
+
};
|
|
228
|
+
/** Parse a task --priority into its raw Odoo selection value. */
|
|
229
|
+
function parseTaskPriority(raw) {
|
|
230
|
+
const v = raw.toLowerCase();
|
|
231
|
+
if (v === "0" || v === "normal" || v === "low")
|
|
232
|
+
return "0";
|
|
233
|
+
if (v === "1" || v === "high")
|
|
234
|
+
return "1";
|
|
235
|
+
throw usageError(`invalid --priority ${JSON.stringify(raw)}`, "expected one of: 0 (normal/low), 1 (high)");
|
|
236
|
+
}
|
|
237
|
+
function parseCount(raw, flag) {
|
|
238
|
+
if (!/^\d+$/.test(raw)) {
|
|
239
|
+
throw usageError(`invalid ${flag} value ${JSON.stringify(raw)}`, "expected a non-negative integer");
|
|
240
|
+
}
|
|
241
|
+
return Number(raw);
|
|
242
|
+
}
|
|
243
|
+
function parseFieldList(raw) {
|
|
244
|
+
const fields = raw
|
|
245
|
+
.split(",")
|
|
246
|
+
.map((f) => f.trim())
|
|
247
|
+
.filter((f) => f !== "");
|
|
248
|
+
if (fields.length === 0) {
|
|
249
|
+
throw usageError("--fields requires at least one field name");
|
|
250
|
+
}
|
|
251
|
+
return fields;
|
|
252
|
+
}
|
|
253
|
+
/** Resolve a project by numeric id or exact name match (mirrors create). */
|
|
254
|
+
async function resolveProjectId(client, input) {
|
|
255
|
+
const num = Number.parseInt(input, 10);
|
|
256
|
+
if (!Number.isNaN(num))
|
|
257
|
+
return num;
|
|
258
|
+
const projects = (await client.executeKw("project.project", "search_read", [[["name", "=", input]]], { fields: ["id", "name"], limit: 1 }));
|
|
259
|
+
if (!projects || projects.length === 0) {
|
|
260
|
+
throw notFoundError("project_not_found", `project "${input}" not found`, "check the project name or use project ID");
|
|
261
|
+
}
|
|
262
|
+
return projects[0].id;
|
|
263
|
+
}
|
|
264
|
+
export async function tasksListCommand(opts, globals) {
|
|
265
|
+
const client = await authenticatedClient(globals);
|
|
266
|
+
if (opts.domain && opts.filter && opts.filter.length > 0) {
|
|
267
|
+
throw usageError("--domain and --filter are mutually exclusive", "pick one filtering form");
|
|
268
|
+
}
|
|
269
|
+
if (opts.open && opts.closed) {
|
|
270
|
+
throw usageError("--open and --closed are mutually exclusive");
|
|
271
|
+
}
|
|
272
|
+
const limit = parseCount(opts.limit, "--limit");
|
|
273
|
+
const offset = parseCount(opts.offset, "--offset");
|
|
274
|
+
// fields_get first: doubles as the module-installed check (exit 4).
|
|
275
|
+
const fieldsMeta = await ModelFields.load(client, "project.task");
|
|
276
|
+
// project_ids (many2many) on project.task.type scopes stage resolution.
|
|
277
|
+
const projectId = await resolveProjectId(client, opts.project);
|
|
278
|
+
logVerbose(`resolved --project "${opts.project}" -> ${projectId}`, globals.verbose);
|
|
279
|
+
const domain = [["project_id", "=", projectId]];
|
|
280
|
+
if (opts.stage) {
|
|
281
|
+
const stageIdNum = Number.parseInt(opts.stage, 10);
|
|
282
|
+
let stageId;
|
|
283
|
+
if (!Number.isNaN(stageIdNum)) {
|
|
284
|
+
stageId = stageIdNum;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const stages = (await client.executeKw("project.task.type", "search_read", [
|
|
288
|
+
[
|
|
289
|
+
["name", "=", opts.stage],
|
|
290
|
+
["project_ids", "in", [projectId]],
|
|
291
|
+
],
|
|
292
|
+
], { fields: ["id", "name"], limit: 1 }));
|
|
293
|
+
if (!stages || stages.length === 0) {
|
|
294
|
+
throw notFoundError("stage_not_found", `stage "${opts.stage}" not found in project ${projectId}`, "check the stage name or use stage ID");
|
|
295
|
+
}
|
|
296
|
+
stageId = stages[0].id;
|
|
297
|
+
}
|
|
298
|
+
domain.push(["stage_id", "=", stageId]);
|
|
299
|
+
logVerbose(`resolved --stage "${opts.stage}" -> ${stageId}`, globals.verbose);
|
|
300
|
+
}
|
|
301
|
+
if (opts.assignee) {
|
|
302
|
+
const user = await resolveReference(client, "res.users", opts.assignee, {
|
|
303
|
+
label: "assignee",
|
|
304
|
+
searchFields: ["login", "email", "name"],
|
|
305
|
+
});
|
|
306
|
+
// many2many membership
|
|
307
|
+
domain.push(["user_ids", "in", [user.id]]);
|
|
308
|
+
logVerbose(`resolved --assignee "${opts.assignee}" -> [${user.id}, "${user.name}"]`, globals.verbose);
|
|
309
|
+
}
|
|
310
|
+
for (const tag of opts.tag ?? []) {
|
|
311
|
+
const resolved = await resolveReference(client, "project.tags", tag, {
|
|
312
|
+
label: "tag",
|
|
313
|
+
});
|
|
314
|
+
domain.push(["tag_ids", "=", resolved.id]);
|
|
315
|
+
logVerbose(`resolved --tag "${tag}" -> [${resolved.id}, "${resolved.name}"]`, globals.verbose);
|
|
316
|
+
}
|
|
317
|
+
if (opts.priority) {
|
|
318
|
+
domain.push(["priority", "=", parseTaskPriority(opts.priority)]);
|
|
319
|
+
}
|
|
320
|
+
if (opts.open)
|
|
321
|
+
domain.push(["is_closed", "=", false]);
|
|
322
|
+
if (opts.closed)
|
|
323
|
+
domain.push(["is_closed", "=", true]);
|
|
324
|
+
if (opts.domain) {
|
|
325
|
+
domain.push(...parseDomain(opts.domain));
|
|
326
|
+
}
|
|
327
|
+
else if (opts.filter && opts.filter.length > 0) {
|
|
328
|
+
domain.push(...buildFilterClauses(opts.filter, fieldsMeta, {
|
|
329
|
+
stage_id: "--stage",
|
|
330
|
+
user_ids: "--assignee",
|
|
331
|
+
tag_ids: "--tag",
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
const fields = opts.fields
|
|
335
|
+
? parseFieldList(opts.fields)
|
|
336
|
+
: TASK_DEFAULT_LIST_FIELDS;
|
|
337
|
+
for (const field of fields) {
|
|
338
|
+
if (field !== "id" && !fieldsMeta.get(field)) {
|
|
339
|
+
throw usageError(`unknown field ${JSON.stringify(field)} on project.task`, `run \`odoo project tasks show <id> --all-fields\` to see available fields`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const order = opts.order ?? "id asc";
|
|
343
|
+
const [records, count] = await Promise.all([
|
|
344
|
+
client.executeKw("project.task", "search_read", [domain], {
|
|
345
|
+
fields,
|
|
346
|
+
limit,
|
|
347
|
+
offset,
|
|
348
|
+
order,
|
|
349
|
+
}),
|
|
350
|
+
client.executeKw("project.task", "search_count", [
|
|
351
|
+
domain,
|
|
352
|
+
]),
|
|
353
|
+
]);
|
|
354
|
+
if (globals.json) {
|
|
355
|
+
printJson({ model: "project.task", records, count, limit, offset });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const headers = fields.map((f) => fieldsMeta.get(f)?.string ?? f);
|
|
359
|
+
const rows = records.map((rec) => fields.map((f) => f === "priority" && typeof rec[f] === "string"
|
|
360
|
+
? (TASK_PRIORITY_LABELS[rec[f]] ?? String(rec[f]))
|
|
361
|
+
: humanCell(rec[f])));
|
|
362
|
+
const color = colorEnabled(globals.noColor);
|
|
363
|
+
process.stdout.write(formatTable(headers, rows, color) + "\n");
|
|
364
|
+
}
|
|
365
|
+
export async function tasksShowCommand(idArg, opts, globals) {
|
|
366
|
+
const client = await authenticatedClient(globals);
|
|
367
|
+
const id = parseId(idArg);
|
|
368
|
+
const fieldsMeta = await ModelFields.load(client, "project.task");
|
|
369
|
+
let fields;
|
|
370
|
+
if (opts.allFields) {
|
|
371
|
+
fields = undefined; // no fields kwarg => everything readable
|
|
372
|
+
}
|
|
373
|
+
else if (opts.fields) {
|
|
374
|
+
fields = parseFieldList(opts.fields);
|
|
375
|
+
for (const field of fields) {
|
|
376
|
+
if (field !== "id" && !fieldsMeta.get(field)) {
|
|
377
|
+
throw usageError(`unknown field ${JSON.stringify(field)} on project.task`, `run \`odoo project tasks show <id> --all-fields\` to see available fields`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Curated view: intersect with fields_get so edition-dependent fields
|
|
383
|
+
// degrade gracefully instead of faulting the read.
|
|
384
|
+
const curated = [
|
|
385
|
+
...new Set([...TASK_DEFAULT_LIST_FIELDS, ...TASK_SHOW_EXTRA_FIELDS]),
|
|
386
|
+
];
|
|
387
|
+
fields = curated.filter((f) => f === "id" || fieldsMeta.get(f));
|
|
388
|
+
}
|
|
389
|
+
const rows = (await client.executeKw("project.task", "read", [[id]], {
|
|
390
|
+
...(fields ? { fields } : {}),
|
|
391
|
+
}));
|
|
392
|
+
if (rows.length === 0) {
|
|
393
|
+
throw notFoundError("record_not_found", `no project.task record with id ${id}`);
|
|
394
|
+
}
|
|
395
|
+
const record = rows[0];
|
|
396
|
+
if (globals.json) {
|
|
397
|
+
printJson({ model: "project.task", record });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const keys = fields ?? Object.keys(record);
|
|
401
|
+
const pairs = keys.map((k) => [
|
|
402
|
+
fieldsMeta.get(k)?.string ?? k,
|
|
403
|
+
k === "priority" && typeof record[k] === "string"
|
|
404
|
+
? (TASK_PRIORITY_LABELS[record[k]] ?? String(record[k]))
|
|
405
|
+
: humanCell(record[k]),
|
|
406
|
+
]);
|
|
407
|
+
const color = colorEnabled(globals.noColor);
|
|
408
|
+
process.stdout.write(formatKeyValues(pairs, color) + "\n");
|
|
409
|
+
}
|
package/dist/mutation.js
CHANGED
|
@@ -40,13 +40,10 @@ export async function buildValues(client, config, spec, opts, mode, globals) {
|
|
|
40
40
|
const resolvedByOption = {};
|
|
41
41
|
const claim = (field, source) => {
|
|
42
42
|
const prev = sourceOf[field];
|
|
43
|
-
if (prev === undefined) {
|
|
43
|
+
if (prev === undefined || prev === "default") {
|
|
44
44
|
sourceOf[field] = source;
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
if (prev === "pinned") {
|
|
48
|
-
throw usageError(`field "${field}" is pinned by this collection and cannot be overridden`, `remove --${kebab(/* context only */ "")}: the ${config.model.split(".")[0]} surface fixes ${field}`);
|
|
49
|
-
}
|
|
50
47
|
throw usageError(`field "${field}" given by both ${prev} and ${source}`, "pass each field exactly once (SPEC §4.2)");
|
|
51
48
|
};
|
|
52
49
|
// 1. Pinned create values first (e.g. type=lead). They cannot be overridden
|
|
@@ -56,6 +53,13 @@ export async function buildValues(client, config, spec, opts, mode, globals) {
|
|
|
56
53
|
values[k] = v;
|
|
57
54
|
sourceOf[k] = "pinned";
|
|
58
55
|
}
|
|
56
|
+
// 1b. Default create values — can be overridden by explicit flags/--set/--values.
|
|
57
|
+
for (const [k, v] of Object.entries(spec.createDefaultValues ?? {})) {
|
|
58
|
+
if (values[k] === undefined) {
|
|
59
|
+
values[k] = v;
|
|
60
|
+
sourceOf[k] = "default";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
// 2. Exclusive groups (--partner vs --customer-*/--customer-name, §6.3).
|
|
61
65
|
for (const group of spec.exclusiveGroups ?? []) {
|