@bergetai/odoo-cli 0.4.1 → 0.5.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
@@ -24,6 +24,13 @@ odoo auth status --json
24
24
  # Unauthenticated connectivity check
25
25
  odoo version
26
26
 
27
+ # Partners (companies and contacts — res.partner)
28
+ odoo partners list --company --json
29
+ odoo partners show 51
30
+ odoo partners create --name "Acme Corp" --is-company true --email info@acme.example
31
+ odoo partners update 51 --email new@acme.example --dry-run
32
+ odoo partners delete 54 --dry-run
33
+
27
34
  # List and inspect records
28
35
  odoo crm leads list --open --limit 20
29
36
  odoo crm leads show 42
@@ -59,6 +66,7 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
59
66
  ## Features
60
67
 
61
68
  - 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
69
+ - Partners surface (`partners list/show/create/update/delete`) — `res.partner` CRUD with `--company`/`--individual` filters, `--archived` (include inactive), first-class flags for `--name`, `--email`, `--phone`, `--website`, `--parent` (m2o), `--is-company`, `--street`, `--city`, `--zip`, `--country` (m2o), `--comment`, full `--dry-run` support, `--fields`/`--all-fields`, table/JSON output
62
70
  - 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
63
71
  - Read-only Helpdesk surface (`helpdesk tickets list/show`) — `--open`/`--closed` via stage `fold`, strict team-scoped stage resolution, module-not-installed error (exit 4)
64
72
  - Read-only Project surface (`project list/show`) — `--user` (project manager) and `--tag` filters, archived projects excluded by default, module-not-installed error (exit 4)
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { activitiesListCommand, activityDoneCommand, scheduleCommand, } from "./
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";
11
11
  import { versionCommand } from "./commands/version.js";
12
+ import { partnersCreateCommand, partnersDeleteCommand, partnersListCommand, partnersShowCommand, partnersUpdateCommand, resPartner, } from "./commands/partners.js";
12
13
  import { CliError } from "./errors.js";
13
14
  import { applyGlobalOptions, enforceTransportSecurity, DEFAULT_TIMEOUT, } from "./global-options.js";
14
15
  import { printError } from "./output.js";
@@ -552,6 +553,52 @@ Examples:
552
553
  .action(async (opts) => {
553
554
  await run((g) => uploadCommand(opts, g), program);
554
555
  });
556
+ const partners = program
557
+ .command("partners")
558
+ .description("Contacts and companies (res.partner)");
559
+ partners
560
+ .command("list")
561
+ .description("search and list partners with curated default columns")
562
+ .option("--archived", "include archived partners (active = false) instead of active only")
563
+ .option("--company", "only companies (is_company = true)")
564
+ .option("--individual", "only individuals (is_company = false)")
565
+ .option("--filter <f=v>", "equality filter; repeatable, AND-ed", collect, [])
566
+ .option("--domain <json>", "Odoo domain as a JSON array (mutually exclusive with --filter)")
567
+ .option("--fields <a,b,c>", "columns to display")
568
+ .option("--limit <n>", "max records (default: 50)", "50")
569
+ .option("--offset <n>", "skip the first n records", "0")
570
+ .option("--order <order>", 'sort order, e.g. "name asc"', undefined)
571
+ .action(async (opts) => {
572
+ await run((g) => partnersListCommand(opts, g), program);
573
+ });
574
+ partners
575
+ .command("show")
576
+ .description("read one partner")
577
+ .argument("<id>", "record id")
578
+ .option("--fields <a,b,c>", "fields to display")
579
+ .option("--all-fields", "display every readable field")
580
+ .action(async (id, opts) => {
581
+ await run((g) => partnersShowCommand(id, opts, g), program);
582
+ });
583
+ addMutationFlags(partners
584
+ .command("create")
585
+ .description("create a partner (company or individual; preview with --dry-run)"), resPartner).action(async (opts) => {
586
+ await run((g) => partnersCreateCommand(opts, g), program);
587
+ });
588
+ addMutationFlags(partners
589
+ .command("update")
590
+ .description("update partners (preview with --dry-run)"), resPartner)
591
+ .argument("<id...>", "record ids")
592
+ .action(async (ids, opts) => {
593
+ await run((g) => partnersUpdateCommand(ids, opts, g), program);
594
+ });
595
+ partners
596
+ .command("delete")
597
+ .description("delete partners (preview with --dry-run; unknown ids are skipped)")
598
+ .argument("<id...>", "record ids")
599
+ .action(async (ids) => {
600
+ await run((g) => partnersDeleteCommand(ids, g), program);
601
+ });
555
602
  return program;
556
603
  }
557
604
  async function run(fn, program) {
@@ -0,0 +1,244 @@
1
+ import { ModelFields } from "../model.js";
2
+ import { authenticatedClient, showCommand, } from "../resource.js";
3
+ import { createCommand, deleteCommand, 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 parseYesNoFlag(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
+ /**
17
+ * Mutation flags for res.partner. Keeps a focused first-class surface — the
18
+ * full partner model has hundreds of fields; `--set` and `--values` cover the
19
+ * long tail. `--parent` resolves the parent company (many2one), and
20
+ * `--is-company` is a boolean flag matching the Odoo `is_company` selection.
21
+ */
22
+ const resPartnerMutation = {
23
+ flags: [
24
+ {
25
+ flags: "--name <name>",
26
+ option: "name",
27
+ field: "name",
28
+ help: "Partner name",
29
+ requiredOnCreate: true,
30
+ },
31
+ {
32
+ flags: "--email <address>",
33
+ option: "email",
34
+ field: "email",
35
+ help: "Email",
36
+ },
37
+ {
38
+ flags: "--phone <number>",
39
+ option: "phone",
40
+ field: "phone",
41
+ help: "Phone",
42
+ },
43
+ {
44
+ flags: "--website <url>",
45
+ option: "website",
46
+ field: "website",
47
+ help: "Website",
48
+ },
49
+ {
50
+ flags: "--parent <name|id>",
51
+ option: "parent",
52
+ field: "parent_id",
53
+ help: "Parent company (name or id)",
54
+ reference: {
55
+ model: "res.partner",
56
+ label: "partner",
57
+ searchFields: ["name", "email"],
58
+ },
59
+ },
60
+ {
61
+ flags: "--is-company <true|false>",
62
+ option: "isCompany",
63
+ field: "is_company",
64
+ help: "Is a company (default: false on create)",
65
+ parse: parseYesNoFlag,
66
+ },
67
+ {
68
+ flags: "--active <true|false>",
69
+ option: "active",
70
+ field: "active",
71
+ help: "Active flag (default: true on create)",
72
+ parse: parseYesNoFlag,
73
+ },
74
+ {
75
+ flags: "--street <text>",
76
+ option: "street",
77
+ field: "street",
78
+ help: "Street",
79
+ },
80
+ {
81
+ flags: "--city <text>",
82
+ option: "city",
83
+ field: "city",
84
+ help: "City",
85
+ },
86
+ {
87
+ flags: "--zip <code>",
88
+ option: "zip",
89
+ field: "zip",
90
+ help: "Postal / ZIP code",
91
+ },
92
+ {
93
+ flags: "--country <name|id>",
94
+ option: "country",
95
+ field: "country_id",
96
+ help: "Country (name or id)",
97
+ reference: {
98
+ model: "res.country",
99
+ label: "country",
100
+ },
101
+ },
102
+ {
103
+ flags: "--comment <text>",
104
+ option: "comment",
105
+ field: "comment",
106
+ help: "Internal comment",
107
+ },
108
+ ],
109
+ createDefaultValues: {
110
+ is_company: false,
111
+ active: true,
112
+ },
113
+ };
114
+ export const resPartner = {
115
+ model: "res.partner",
116
+ showCommand: "odoo partners show",
117
+ defaultListFields: [
118
+ "id",
119
+ "name",
120
+ "is_company",
121
+ "email",
122
+ "phone",
123
+ "parent_id",
124
+ "active",
125
+ "create_date",
126
+ ],
127
+ showExtraFields: [
128
+ "website",
129
+ "street",
130
+ "city",
131
+ "zip",
132
+ "country_id",
133
+ "comment",
134
+ "company_id",
135
+ "write_date",
136
+ ],
137
+ resolutionHints: {
138
+ parent_id: "--parent",
139
+ country_id: "--country",
140
+ },
141
+ priorityLabels: {},
142
+ pinnedDomain: [],
143
+ references: {
144
+ assignee: {
145
+ model: "res.partner",
146
+ label: "partner",
147
+ field: "parent_id",
148
+ searchFields: ["name", "email"],
149
+ },
150
+ },
151
+ mutation: resPartnerMutation,
152
+ };
153
+ function parseCount(raw, flag) {
154
+ if (!/^\d+$/.test(raw)) {
155
+ throw usageError(`invalid ${flag} value ${JSON.stringify(raw)}`, "expected a non-negative integer");
156
+ }
157
+ return Number(raw);
158
+ }
159
+ function parseFieldList(raw) {
160
+ const fields = raw
161
+ .split(",")
162
+ .map((f) => f.trim())
163
+ .filter((f) => f !== "");
164
+ if (fields.length === 0) {
165
+ throw usageError("--fields requires at least one field name");
166
+ }
167
+ return fields;
168
+ }
169
+ export async function partnersListCommand(opts, globals) {
170
+ const client = await authenticatedClient(globals);
171
+ if (opts.domain && opts.filter && opts.filter.length > 0) {
172
+ throw usageError("--domain and --filter are mutually exclusive", "pick one filtering form");
173
+ }
174
+ if (opts.company && opts.individual) {
175
+ throw usageError("--company and --individual are mutually exclusive", "pick one (or neither to list both)");
176
+ }
177
+ const limit = parseCount(opts.limit, "--limit");
178
+ const offset = parseCount(opts.offset, "--offset");
179
+ const fieldsMeta = await ModelFields.load(client, "res.partner");
180
+ const domain = [];
181
+ if (opts.archived) {
182
+ domain.push(["active", "=", false]);
183
+ }
184
+ else {
185
+ domain.push(["active", "=", true]);
186
+ }
187
+ if (opts.company)
188
+ domain.push(["is_company", "=", true]);
189
+ if (opts.individual)
190
+ domain.push(["is_company", "=", false]);
191
+ if (opts.domain) {
192
+ domain.push(...parseDomain(opts.domain));
193
+ }
194
+ else if (opts.filter && opts.filter.length > 0) {
195
+ domain.push(...buildFilterClauses(opts.filter, fieldsMeta, {
196
+ parent_id: "--parent",
197
+ country_id: "--country",
198
+ }));
199
+ }
200
+ const fields = opts.fields
201
+ ? parseFieldList(opts.fields)
202
+ : resPartner.defaultListFields;
203
+ for (const field of fields) {
204
+ if (field !== "id" && !fieldsMeta.get(field)) {
205
+ throw usageError(`unknown field ${JSON.stringify(field)} on res.partner`, `run \`odoo partners show <id> --all-fields\` to see available fields`);
206
+ }
207
+ }
208
+ const order = opts.order ?? "id asc";
209
+ // active_test:false so Odoo returns both active and archived rows; the
210
+ // domain above is the authoritative filter.
211
+ const kwargs = {
212
+ fields,
213
+ limit,
214
+ offset,
215
+ order,
216
+ context: { active_test: false },
217
+ };
218
+ const [records, count] = await Promise.all([
219
+ client.executeKw("res.partner", "search_read", [domain], kwargs),
220
+ client.executeKw("res.partner", "search_count", [domain], {
221
+ context: { active_test: false },
222
+ }),
223
+ ]);
224
+ if (globals.json) {
225
+ printJson({ model: "res.partner", records, count, limit, offset });
226
+ return;
227
+ }
228
+ const headers = fields.map((f) => fieldsMeta.get(f)?.string ?? f);
229
+ const rows = records.map((rec) => fields.map((f) => humanCell(rec[f])));
230
+ const color = colorEnabled(globals.noColor);
231
+ process.stdout.write(formatTable(headers, rows, color) + "\n");
232
+ }
233
+ export function partnersShowCommand(idArg, opts, globals) {
234
+ return showCommand(resPartner, idArg, opts, globals);
235
+ }
236
+ export function partnersCreateCommand(opts, globals) {
237
+ return createCommand(resPartner, opts, globals);
238
+ }
239
+ export function partnersUpdateCommand(idArgs, opts, globals) {
240
+ return updateCommand(resPartner, idArgs, opts, globals);
241
+ }
242
+ export function partnersDeleteCommand(idArgs, globals) {
243
+ return deleteCommand(resPartner, idArgs, globals);
244
+ }
@@ -168,9 +168,10 @@ export async function createTaskCommand(opts, globals) {
168
168
  }
169
169
  return;
170
170
  }
171
- const taskId = (await client.executeKw("project.task", "create", [
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bergetai/odoo-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Agent-first CLI for the Odoo External API",
5
5
  "publishConfig": {
6
6
  "access": "public"