@bergetai/odoo-cli 0.3.3 → 0.4.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 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)");
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bergetai/odoo-cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Agent-first CLI for the Odoo External API",
5
5
  "publishConfig": {
6
6
  "access": "public"