@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 +8 -0
- package/dist/cli.js +47 -0
- package/dist/commands/partners.js +244 -0
- package/dist/commands/project.js +2 -1
- package/package.json +1 -1
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
|
+
}
|
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,
|