@bergetai/odoo-cli 0.3.3 → 0.4.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 +8 -0
- package/dist/cli.js +43 -0
- package/dist/commands/project.js +2 -1
- package/dist/commands/teams.js +199 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,13 @@ odoo crm leads create --name "New lead" --email info@example.com
|
|
|
40
40
|
odoo crm leads update 42 --stage Won
|
|
41
41
|
odoo crm leads delete 42 --yes
|
|
42
42
|
|
|
43
|
+
# Sales teams (crm.team)
|
|
44
|
+
odoo crm teams list
|
|
45
|
+
odoo crm teams show 5
|
|
46
|
+
odoo crm teams create --name "Partnerships" --team-leader alice@acme.com
|
|
47
|
+
odoo crm teams update 5 --name "Direct Sales (renamed)" --dry-run
|
|
48
|
+
odoo crm teams delete 5 --dry-run
|
|
49
|
+
|
|
43
50
|
# Internal notes (NEVER sends email to customer)
|
|
44
51
|
odoo helpdesk tickets add-note 398 --body "Internal note text"
|
|
45
52
|
odoo helpdesk tickets add-note 398 --body-file note.md
|
|
@@ -52,6 +59,7 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
|
|
|
52
59
|
## Features
|
|
53
60
|
|
|
54
61
|
- 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
|
|
62
|
+
- Sales teams surface (`crm teams list/show/create/update/delete`) — `--archived` (include inactive teams), `--team-leader` (team leader, login/email/name), `--active`/`--use-leads`/`--use-opportunities`/`--invoiced-target`/`--color`/`--sequence`, full `--dry-run` support, `--fields`/`--all-fields`, table/JSON output
|
|
55
63
|
- Read-only Helpdesk surface (`helpdesk tickets list/show`) — `--open`/`--closed` via stage `fold`, strict team-scoped stage resolution, module-not-installed error (exit 4)
|
|
56
64
|
- 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
65
|
- 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)
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command, CommanderError } from "commander";
|
|
3
3
|
import { authLoginCommand, authStatusCommand } from "./commands/auth.js";
|
|
4
4
|
import { leadsCreateCommand, leadsDeleteCommand, leadsListCommand, leadsShowCommand, leadsUpdateCommand, crmLeads, } from "./commands/crm.js";
|
|
5
|
+
import { teamsCreateCommand, teamsDeleteCommand, teamsListCommand, teamsShowCommand, teamsUpdateCommand, crmTeams, } from "./commands/teams.js";
|
|
5
6
|
import { ticketsCreateCommand, ticketsDeleteCommand, ticketsListCommand, ticketsShowCommand, ticketsUpdateCommand, helpdeskTickets, } from "./commands/helpdesk.js";
|
|
6
7
|
import { addNoteCommand, listMessagesCommand, } from "./commands/notes.js";
|
|
7
8
|
import { activitiesListCommand, activityDoneCommand, scheduleCommand, } from "./commands/activities.js";
|
|
@@ -225,6 +226,48 @@ Examples:
|
|
|
225
226
|
.action(async (ids) => {
|
|
226
227
|
await run((g) => activityDoneCommand(ids, g), program);
|
|
227
228
|
});
|
|
229
|
+
const teams = crm.command("teams").description("sales teams (crm.team)");
|
|
230
|
+
teams
|
|
231
|
+
.command("list")
|
|
232
|
+
.description("search and list sales teams with curated default columns")
|
|
233
|
+
.option("--archived", "include archived teams (active = false) instead of active only")
|
|
234
|
+
.option("--domain <json>", "Odoo domain as a JSON array")
|
|
235
|
+
.option("--filter <f=v>", "equality filter; repeatable, AND-ed", collect, [])
|
|
236
|
+
.option("--fields <a,b,c>", "columns to display")
|
|
237
|
+
.option("--limit <n>", "max records (default: 50)", "50")
|
|
238
|
+
.option("--offset <n>", "skip the first n records", "0")
|
|
239
|
+
.option("--order <order>", 'sort order, e.g. "name asc"', undefined)
|
|
240
|
+
.action(async (opts) => {
|
|
241
|
+
await run((g) => teamsListCommand(opts, g), program);
|
|
242
|
+
});
|
|
243
|
+
teams
|
|
244
|
+
.command("show")
|
|
245
|
+
.description("read one sales team")
|
|
246
|
+
.argument("<id>", "record id")
|
|
247
|
+
.option("--fields <a,b,c>", "fields to display")
|
|
248
|
+
.option("--all-fields", "display every readable field")
|
|
249
|
+
.action(async (id, opts) => {
|
|
250
|
+
await run((g) => teamsShowCommand(id, opts, g), program);
|
|
251
|
+
});
|
|
252
|
+
addMutationFlags(teams
|
|
253
|
+
.command("create")
|
|
254
|
+
.description("create a sales team (preview with --dry-run)"), crmTeams).action(async (opts) => {
|
|
255
|
+
await run((g) => teamsCreateCommand(opts, g), program);
|
|
256
|
+
});
|
|
257
|
+
addMutationFlags(teams
|
|
258
|
+
.command("update")
|
|
259
|
+
.description("update sales teams (preview with --dry-run)"), crmTeams)
|
|
260
|
+
.argument("<id...>", "record ids")
|
|
261
|
+
.action(async (ids, opts) => {
|
|
262
|
+
await run((g) => teamsUpdateCommand(ids, opts, g), program);
|
|
263
|
+
});
|
|
264
|
+
teams
|
|
265
|
+
.command("delete")
|
|
266
|
+
.description("delete sales teams (preview with --dry-run; unknown ids are skipped)")
|
|
267
|
+
.argument("<id...>", "record ids")
|
|
268
|
+
.action(async (ids) => {
|
|
269
|
+
await run((g) => teamsDeleteCommand(ids, g), program);
|
|
270
|
+
});
|
|
228
271
|
const helpdesk = program
|
|
229
272
|
.command("helpdesk")
|
|
230
273
|
.description("Helpdesk app (helpdesk.ticket, Enterprise)");
|
package/dist/commands/project.js
CHANGED
|
@@ -168,9 +168,10 @@ export async function createTaskCommand(opts, globals) {
|
|
|
168
168
|
}
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
|
-
const
|
|
171
|
+
const rawTaskId = (await client.executeKw("project.task", "create", [
|
|
172
172
|
[taskValues],
|
|
173
173
|
]));
|
|
174
|
+
const taskId = Array.isArray(rawTaskId) ? rawTaskId[0] : rawTaskId;
|
|
174
175
|
const result = {
|
|
175
176
|
task_id: taskId,
|
|
176
177
|
name: opts.name,
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { ModelFields } from "../model.js";
|
|
2
|
+
import { authenticatedClient, showCommand, } from "../resource.js";
|
|
3
|
+
import { createCommand, deleteCommand, parseDecimal, updateCommand, } from "../mutation.js";
|
|
4
|
+
import { buildFilterClauses, parseDomain } from "../filter.js";
|
|
5
|
+
import { usageError } from "../errors.js";
|
|
6
|
+
import { colorEnabled, printJson } from "../output.js";
|
|
7
|
+
import { formatTable, humanCell } from "../table.js";
|
|
8
|
+
function parseBooleanFlag(raw, flagName) {
|
|
9
|
+
const v = raw.toLowerCase();
|
|
10
|
+
if (v === "true" || v === "1" || v === "yes")
|
|
11
|
+
return true;
|
|
12
|
+
if (v === "false" || v === "0" || v === "no")
|
|
13
|
+
return false;
|
|
14
|
+
throw usageError(`invalid ${flagName} value ${JSON.stringify(raw)}`, "expected: true or false");
|
|
15
|
+
}
|
|
16
|
+
const crmTeamsMutation = {
|
|
17
|
+
flags: [
|
|
18
|
+
{
|
|
19
|
+
flags: "--name <name>",
|
|
20
|
+
option: "name",
|
|
21
|
+
field: "name",
|
|
22
|
+
help: "Sales team name",
|
|
23
|
+
requiredOnCreate: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
flags: "--team-leader <name|id>",
|
|
27
|
+
option: "teamLeader",
|
|
28
|
+
field: "user_id",
|
|
29
|
+
help: "Team leader (login, email, or name)",
|
|
30
|
+
reference: {
|
|
31
|
+
model: "res.users",
|
|
32
|
+
label: "user",
|
|
33
|
+
searchFields: ["login", "email", "name"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
flags: "--active <true|false>",
|
|
38
|
+
option: "active",
|
|
39
|
+
field: "active",
|
|
40
|
+
help: "Active flag (default: true on create)",
|
|
41
|
+
parse: parseBooleanFlag,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
flags: "--use-leads <true|false>",
|
|
45
|
+
option: "useLeads",
|
|
46
|
+
field: "use_leads",
|
|
47
|
+
help: "Enable leads",
|
|
48
|
+
parse: parseBooleanFlag,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
flags: "--use-opportunities <true|false>",
|
|
52
|
+
option: "useOpportunities",
|
|
53
|
+
field: "use_opportunities",
|
|
54
|
+
help: "Enable pipeline (opportunities)",
|
|
55
|
+
parse: parseBooleanFlag,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
flags: "--invoiced-target <amount>",
|
|
59
|
+
option: "invoicedTarget",
|
|
60
|
+
field: "invoiced_target",
|
|
61
|
+
help: "Invoicing target",
|
|
62
|
+
parse: parseDecimal,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
flags: "--color <index>",
|
|
66
|
+
option: "color",
|
|
67
|
+
field: "color",
|
|
68
|
+
help: "Color index (kanban)",
|
|
69
|
+
parse: parseDecimal,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
flags: "--sequence <n>",
|
|
73
|
+
option: "sequence",
|
|
74
|
+
field: "sequence",
|
|
75
|
+
help: "Sort sequence",
|
|
76
|
+
parse: parseDecimal,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
export const crmTeams = {
|
|
81
|
+
model: "crm.team",
|
|
82
|
+
showCommand: "odoo crm teams show",
|
|
83
|
+
defaultListFields: [
|
|
84
|
+
"id",
|
|
85
|
+
"name",
|
|
86
|
+
"user_id",
|
|
87
|
+
"active",
|
|
88
|
+
"is_favorite",
|
|
89
|
+
"create_date",
|
|
90
|
+
],
|
|
91
|
+
showExtraFields: [
|
|
92
|
+
"company_id",
|
|
93
|
+
"use_leads",
|
|
94
|
+
"use_opportunities",
|
|
95
|
+
"invoiced_target",
|
|
96
|
+
"color",
|
|
97
|
+
"sequence",
|
|
98
|
+
"member_ids",
|
|
99
|
+
"write_date",
|
|
100
|
+
],
|
|
101
|
+
resolutionHints: {
|
|
102
|
+
user_id: "--team-leader",
|
|
103
|
+
},
|
|
104
|
+
priorityLabels: {},
|
|
105
|
+
pinnedDomain: [],
|
|
106
|
+
references: {
|
|
107
|
+
assignee: {
|
|
108
|
+
model: "res.users",
|
|
109
|
+
label: "team leader",
|
|
110
|
+
field: "user_id",
|
|
111
|
+
searchFields: ["login", "email", "name"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
mutation: crmTeamsMutation,
|
|
115
|
+
};
|
|
116
|
+
function parseCount(raw, flag) {
|
|
117
|
+
if (!/^\d+$/.test(raw)) {
|
|
118
|
+
throw usageError(`invalid ${flag} value ${JSON.stringify(raw)}`, "expected a non-negative integer");
|
|
119
|
+
}
|
|
120
|
+
return Number(raw);
|
|
121
|
+
}
|
|
122
|
+
function parseFieldList(raw) {
|
|
123
|
+
const fields = raw
|
|
124
|
+
.split(",")
|
|
125
|
+
.map((f) => f.trim())
|
|
126
|
+
.filter((f) => f !== "");
|
|
127
|
+
if (fields.length === 0) {
|
|
128
|
+
throw usageError("--fields requires at least one field name");
|
|
129
|
+
}
|
|
130
|
+
return fields;
|
|
131
|
+
}
|
|
132
|
+
export async function teamsListCommand(opts, globals) {
|
|
133
|
+
const client = await authenticatedClient(globals);
|
|
134
|
+
if (opts.domain && opts.filter && opts.filter.length > 0) {
|
|
135
|
+
throw usageError("--domain and --filter are mutually exclusive", "pick one filtering form");
|
|
136
|
+
}
|
|
137
|
+
const limit = parseCount(opts.limit, "--limit");
|
|
138
|
+
const offset = parseCount(opts.offset, "--offset");
|
|
139
|
+
const fieldsMeta = await ModelFields.load(client, "crm.team");
|
|
140
|
+
const domain = [];
|
|
141
|
+
if (opts.archived) {
|
|
142
|
+
domain.push(["active", "=", false]);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
domain.push(["active", "=", true]);
|
|
146
|
+
}
|
|
147
|
+
if (opts.domain) {
|
|
148
|
+
domain.push(...parseDomain(opts.domain));
|
|
149
|
+
}
|
|
150
|
+
else if (opts.filter && opts.filter.length > 0) {
|
|
151
|
+
domain.push(...buildFilterClauses(opts.filter, fieldsMeta, {
|
|
152
|
+
user_id: "--team-leader",
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
const fields = opts.fields
|
|
156
|
+
? parseFieldList(opts.fields)
|
|
157
|
+
: crmTeams.defaultListFields;
|
|
158
|
+
for (const field of fields) {
|
|
159
|
+
if (field !== "id" && !fieldsMeta.get(field)) {
|
|
160
|
+
throw usageError(`unknown field ${JSON.stringify(field)} on crm.team`, `run \`odoo crm teams show <id> --all-fields\` to see available fields`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const order = opts.order ?? "id asc";
|
|
164
|
+
// active_test:false so Odoo returns both active and archived rows; the
|
|
165
|
+
// domain above does the actual active filtering.
|
|
166
|
+
const kwargs = {
|
|
167
|
+
fields,
|
|
168
|
+
limit,
|
|
169
|
+
offset,
|
|
170
|
+
order,
|
|
171
|
+
context: { active_test: false },
|
|
172
|
+
};
|
|
173
|
+
const [records, count] = await Promise.all([
|
|
174
|
+
client.executeKw("crm.team", "search_read", [domain], kwargs),
|
|
175
|
+
client.executeKw("crm.team", "search_count", [domain], {
|
|
176
|
+
context: { active_test: false },
|
|
177
|
+
}),
|
|
178
|
+
]);
|
|
179
|
+
if (globals.json) {
|
|
180
|
+
printJson({ model: "crm.team", records, count, limit, offset });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const headers = fields.map((f) => fieldsMeta.get(f)?.string ?? f);
|
|
184
|
+
const rows = records.map((rec) => fields.map((f) => humanCell(rec[f])));
|
|
185
|
+
const color = colorEnabled(globals.noColor);
|
|
186
|
+
process.stdout.write(formatTable(headers, rows, color) + "\n");
|
|
187
|
+
}
|
|
188
|
+
export function teamsShowCommand(idArg, opts, globals) {
|
|
189
|
+
return showCommand(crmTeams, idArg, opts, globals);
|
|
190
|
+
}
|
|
191
|
+
export function teamsCreateCommand(opts, globals) {
|
|
192
|
+
return createCommand(crmTeams, opts, globals);
|
|
193
|
+
}
|
|
194
|
+
export function teamsUpdateCommand(idArgs, opts, globals) {
|
|
195
|
+
return updateCommand(crmTeams, idArgs, opts, globals);
|
|
196
|
+
}
|
|
197
|
+
export function teamsDeleteCommand(idArgs, globals) {
|
|
198
|
+
return deleteCommand(crmTeams, idArgs, globals);
|
|
199
|
+
}
|