@bergetai/odoo-cli 0.12.1 → 0.12.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 +30 -0
- package/dist/cli.js +248 -0
- package/dist/commands/sales.js +902 -0
- package/dist/commands/sign.js +599 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,17 @@ odoo documents records list --folder "Projects > Product Backlog" # nested path
|
|
|
60
60
|
odoo documents records show 25
|
|
61
61
|
odoo documents records download 25 --out ./spec.pdf
|
|
62
62
|
|
|
63
|
+
# Sales (sale.order) — quotations; sending stays a human step in the Odoo UI
|
|
64
|
+
odoo sales orders list --state sent --partner "Softmizer" --json
|
|
65
|
+
odoo sales orders read 11515 # header + partner + order lines
|
|
66
|
+
odoo sales orders create --partner 17682 --plan Monthly --lines lines.json --dry-run
|
|
67
|
+
odoo sales orders update 11515 --set plan_id=2 --dry-run
|
|
68
|
+
odoo sales lines add 11515 --lines extra.json
|
|
69
|
+
odoo sales lines update 11818 --lines fix.json
|
|
70
|
+
odoo sales plans list
|
|
71
|
+
odoo sales invoices precedent --product 188 # invoicing currency history
|
|
72
|
+
odoo sales pdf 11515 --out ./quotation.pdf
|
|
73
|
+
|
|
63
74
|
# Mutations (support --dry-run)
|
|
64
75
|
odoo crm leads create --name "New lead" --email info@example.com
|
|
65
76
|
odoo crm leads update 42 --stage Won
|
|
@@ -144,6 +155,25 @@ Note: `odoo --version` prints the CLI's own version; `odoo version` queries the
|
|
|
144
155
|
|
|
145
156
|
## Features
|
|
146
157
|
|
|
158
|
+
- Sales surface (`sales`) — quotations and sales orders (`sale.order`) without the raw XML-RPC plumbing: `orders list` (curated columns incl. pricelist/currency, `--partner`/`--state` filters plus `--filter`/`--domain`), `orders read` (full header, partner with VAT/address, and the order lines with sections/notes), `orders create` (first-class flags for `--partner`/`--pricelist`/`--payment-terms`/`--validity`/`--opportunity`/`--salesperson`/`--plan`/`--note`, order lines as a JSON array via `--lines <file>`/`-` supporting `line_section`/`line_note` lines and product lines with `tax_ids`; missing line name/price default from the product), `orders update` (header-only `--set` with an allowlist — setting `state` is refused by design: confirm/cancel is a human step in the Odoo UI), `lines add`/`lines update` (same JSON line format), `plans list` (`sale.subscription.plan`), `invoices precedent --product` (posted customer-invoice lines for a product, newest first — invoicing currency precedent), and `pdf <id> --out <path>` (portal quotation PDF via the order's `access_token`, verified `%PDF` magic). **Sending quotations is deliberately not implemented** — it is done by a human via 'Send by Email' in the Odoo UI (gives the customer Accept & Sign), CLI policy 2026-09-24
|
|
159
|
+
- Signature pipeline (`sign`) — Odoo Sign with safety rails learned from live incidents:
|
|
160
|
+
- `sign templates list/show` and `sign templates create-from-document <id>` — builds a
|
|
161
|
+
`sign.template` from a Documents file, converting non-PDF files (`.docx` etc.) to PDF through
|
|
162
|
+
the self-hosted OnlyOffice Document Server (connection + JWT read from
|
|
163
|
+
`ir.config_parameter` `euro_office_connector.*`; temp attachment with `access_token`,
|
|
164
|
+
cleaned up after conversion)
|
|
165
|
+
- `sign items-add <template> --file items.json` — place signature/date/text fields from a JSON
|
|
166
|
+
spec (coordinates in % of page; type and role resolve by id, `item_type`, or name). Added via
|
|
167
|
+
the template's one2many because direct `sign.item.create` hits an ACL on this build
|
|
168
|
+
- `sign requests list` and `sign requests create <template>` — **no draft state exists**:
|
|
169
|
+
creating a request with partners in default state emails them in the same transaction
|
|
170
|
+
(verified incident 2026-09-23). Default is therefore a `shared` link (anonymous signer, no
|
|
171
|
+
email) with a printed sign URL; `--state sent --signer … --role … --subject …` requires an
|
|
172
|
+
explicit `--confirm-send` and goes through the `sign.send.request` wizard so the email
|
|
173
|
+
carries a subject AND the signing link (a bare state flip sends a body-less email). A UTC
|
|
174
|
+
`mail.mail` audit is printed after send; `sign requests cancel <id>` is the safety valve.
|
|
175
|
+
Shared links require single-role templates (the guard errors with guidance on multi-role
|
|
176
|
+
templates, since the server only accepts one anonymous signer per link)
|
|
147
177
|
- Footprint overview (`footprint <email>`) — one read-only invocation that enumerates everything tied to an email across the erasure-relevant modules (`res.partner` incl. archived, `res.users`, `crm.lead`, `mailing.*`, `helpdesk.ticket`, `mail.message`, `ir.attachment`), correlates the found partners into the RESTRICT accounting surfaces (`account.move`/`sale.order` incl. invoice & shipping addresses, `account.payment`), and classifies every record by erasure disposition: `delete`, `anonymize` (referenced — shell, never unlink), `keep` (bokföringslagen 7-year retention), `review` (account roots, legal-claim branch, internal users, unverifiable attachment parents). `--json` emits a structured per-group payload (fixed 11-group key order, `totalRecords`, per-group `found`/`total` server count/`truncated`/`dispositions` tally, many2one tuples normalized to `{id, name}`) with a `commandHint` on each actionable record, a top-level `runOrder` (SOP execution order), and decision-grade fields on partner records (`child_ids`/`user_ids`/`vat`). Review records with out-of-footprint parents are resolved inline — the parent's name is embedded in the reason (bounded by a per-model fetch cap; beyond it records keep an `inspectHint`). Every record tells you what to run next — or already answers the question. Read-only by design: verification (art. 12.6) is a human step and the argument must already be the verified email; record-rule hidden rows (e.g. `user_notification` messages) mean the raw SQL pre-flight stays the authoritative evidence. Never mutates anything
|
|
148
178
|
- 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
|
|
149
179
|
- Partners surface (`partners list/show/tags/create/update/delete`) — `res.partner` CRUD with `--company`/`--individual` filters, `--search` (case-insensitive name/email match), `--tag` (repeatable, resolves `res.partner.category` by exact name then ilike), `--archived` (include inactive), curated list columns (ID, Name, Company Type, City, Email, Is a Company, Tags, Salesperson), curated `show` view with tag names, salesperson, customer/supplier ranks plus every populated `x_studio*` custom field, `partners tags` (usage counts via `read_group`, sorted by usage), first-class mutation flags for `--name`, `--email`, `--phone`, `--website`, `--parent` (m2o), `--is-company`, `--street`, `--city`, `--zip`, `--country` (m2o), `--comment`, `--add-tag`/`--remove-tag` on update (unknown tags fail on add; removal silently skips tags the record lacks; protected fields `company_type` and `x_studio_invoice_approved` refuse writes), full `--dry-run` support, `--fields`/`--all-fields`, table/JSON output
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ import { versionCommand } from "./commands/version.js";
|
|
|
12
12
|
import { partnersCreateCommand, partnersDeleteCommand, partnersListCommand, partnersShowCommand, partnersTagsCommand, partnersUpdateCommand, resPartner, } from "./commands/partners.js";
|
|
13
13
|
import { usersDeleteCommand, usersShowCommand, } from "./commands/users.js";
|
|
14
14
|
import { attachmentsDeleteCommand, attachmentsDownloadCommand, attachmentsListCommand, attachmentsShowCommand, } from "./commands/attachments.js";
|
|
15
|
+
import { signTemplatesCreateFromDocumentCommand, signTemplatesListCommand, signTemplatesShowCommand, signItemsAddCommand, signRequestsCreateCommand, signRequestsListCommand, signRequestsCancelCommand, } from "./commands/sign.js";
|
|
16
|
+
import { invoicesPrecedentCommand, linesAddCommand, linesUpdateCommand, ordersCreateCommand, ordersListCommand, ordersReadCommand, ordersUpdateCommand, pdfCommand, plansListCommand, } from "./commands/sales.js";
|
|
15
17
|
import { messagesDeleteCommand, messagesListCommand, messagesScrubCommand, messagesShowCommand, } from "./commands/messages.js";
|
|
16
18
|
import { contactsDeleteCommand, contactsListCommand, contactsShowCommand, listsListCommand, listsShowCommand, tracesDeleteCommand, tracesListCommand, tracesShowCommand, } from "./commands/marketing.js";
|
|
17
19
|
import { modelsFieldsCommand, modelsReadCommand, } from "./commands/models.js";
|
|
@@ -821,6 +823,252 @@ Examples:
|
|
|
821
823
|
.action(async (ids) => {
|
|
822
824
|
await run((g) => attachmentsDeleteCommand(ids, g), program);
|
|
823
825
|
});
|
|
826
|
+
const sign = program
|
|
827
|
+
.command("sign")
|
|
828
|
+
.description("Sign app (sign.template, sign.request) — safe signing pipeline");
|
|
829
|
+
const signTemplates = sign
|
|
830
|
+
.command("templates")
|
|
831
|
+
.description("signature templates");
|
|
832
|
+
signTemplates
|
|
833
|
+
.command("list")
|
|
834
|
+
.description("list signature templates")
|
|
835
|
+
.option("--archived", "include archived templates")
|
|
836
|
+
.option("--limit <n>", "max templates (default: 50)", "50")
|
|
837
|
+
.option("--json", "emit a single JSON object on stdout")
|
|
838
|
+
.action(async (opts) => {
|
|
839
|
+
await run((g) => signTemplatesListCommand(opts, g), program);
|
|
840
|
+
});
|
|
841
|
+
signTemplates
|
|
842
|
+
.command("show")
|
|
843
|
+
.description("read one signature template")
|
|
844
|
+
.argument("<id>", "sign.template id")
|
|
845
|
+
.option("--all-fields", "display every readable field")
|
|
846
|
+
.option("--json", "emit a single JSON object on stdout")
|
|
847
|
+
.action(async (id, opts) => {
|
|
848
|
+
await run((g) => signTemplatesShowCommand(id, opts, g), program);
|
|
849
|
+
});
|
|
850
|
+
signTemplates
|
|
851
|
+
.command("create-from-document")
|
|
852
|
+
.description("create a sign.template from a Documents file (converts non-PDF via the Document Server)")
|
|
853
|
+
.argument("<document>", "documents.document id")
|
|
854
|
+
.option("--name <name>", "template name (default: document name)")
|
|
855
|
+
.option("--json", "emit a single JSON object on stdout")
|
|
856
|
+
.action(async (doc, opts) => {
|
|
857
|
+
await run((g) => signTemplatesCreateFromDocumentCommand(doc, opts, g), program);
|
|
858
|
+
});
|
|
859
|
+
sign
|
|
860
|
+
.command("items-add")
|
|
861
|
+
.description("add signature fields from a JSON spec, via the template (direct sign.item writes are ACL-blocked)")
|
|
862
|
+
.argument("<template>", "sign.template id")
|
|
863
|
+
.requiredOption("--file <path>", "JSON items spec (coordinates in % of page)")
|
|
864
|
+
.option("--json", "emit a single JSON object on stdout")
|
|
865
|
+
.addHelpText("after", `
|
|
866
|
+
Spec: [{"type":"signature","page":3,"x":12,"y":65,"width":24,"height":6,
|
|
867
|
+
"role":"Standard","name":"Signature","required":true}]
|
|
868
|
+
"type" accepts a sign.item.type id, item_type (signature/date/text/...),
|
|
869
|
+
or a name; "role" a sign.item.role id or name.
|
|
870
|
+
|
|
871
|
+
Examples:
|
|
872
|
+
$ odoo sign items-add 12 --file items.json --dry-run
|
|
873
|
+
$ odoo sign items-add 12 --file items.json`)
|
|
874
|
+
.action(async (templateId, opts) => {
|
|
875
|
+
await run((g) => signItemsAddCommand(templateId, opts, g), program);
|
|
876
|
+
});
|
|
877
|
+
const signRequests = sign
|
|
878
|
+
.command("requests")
|
|
879
|
+
.description("signature requests");
|
|
880
|
+
signRequests
|
|
881
|
+
.command("list")
|
|
882
|
+
.description("list signature requests (newest first)")
|
|
883
|
+
.option("--state <state>", "filter by state (shared/sent/completed/canceled)")
|
|
884
|
+
.option("--limit <n>", "max requests (default: 30)", "30")
|
|
885
|
+
.option("--json", "emit a single JSON object on stdout")
|
|
886
|
+
.action(async (opts) => {
|
|
887
|
+
await run((g) => signRequestsListCommand(opts, g), program);
|
|
888
|
+
});
|
|
889
|
+
signRequests
|
|
890
|
+
.command("create")
|
|
891
|
+
.description("create a signature request. Default: shared draft link (anonymous, no email). --state sent emails the signers — requires --confirm-send")
|
|
892
|
+
.argument("<template>", "sign.template id")
|
|
893
|
+
.option("-s, --state <state>", "shared (default) or sent")
|
|
894
|
+
.option("--role <name|id>", "role for shared links (exactly one); repeatable for --state sent", collect, [])
|
|
895
|
+
.option("--signer <name|id>", "signer (partner email or id) for --state sent; one per --role", collect, [])
|
|
896
|
+
.option("--subject <subject>", "email subject (required with --state sent)")
|
|
897
|
+
.option("--message <text>", "email message body (optional)")
|
|
898
|
+
.option("--confirm-send", "required with --state sent — emails go out immediately")
|
|
899
|
+
.option("--json", "emit a single JSON object on stdout")
|
|
900
|
+
.addHelpText("after", `
|
|
901
|
+
sign.request has NO draft state: creating a request with partners emails
|
|
902
|
+
them in the same transaction. Shared links are the only safe pre-state.
|
|
903
|
+
|
|
904
|
+
Examples:
|
|
905
|
+
$ odoo sign requests create 12 --role Customer --json
|
|
906
|
+
$ odoo sign requests create 12 --state sent --subject "Sign" \\
|
|
907
|
+
--signer markus@example.com --role Customer --confirm-send`)
|
|
908
|
+
.action(async (templateId, opts) => {
|
|
909
|
+
await run((g) => signRequestsCreateCommand(templateId, opts, g), program);
|
|
910
|
+
});
|
|
911
|
+
signRequests
|
|
912
|
+
.command("cancel")
|
|
913
|
+
.description("cancel a signature request (safety valve; preview with --dry-run)")
|
|
914
|
+
.argument("<id>", "sign.request id")
|
|
915
|
+
.option("--json", "emit a single JSON object on stdout")
|
|
916
|
+
.action(async (id, opts) => {
|
|
917
|
+
await run((g) => signRequestsCancelCommand(id, opts, g), program);
|
|
918
|
+
});
|
|
919
|
+
const sales = program
|
|
920
|
+
.command("sales")
|
|
921
|
+
.description("Sales app (sale.order, sale.order.line, sale.subscription.plan)")
|
|
922
|
+
.addHelpText("after", `
|
|
923
|
+
Sending quotations is done by a human via 'Send by Email' in the Odoo UI
|
|
924
|
+
(gives the customer Accept & Sign). Not available in this CLI by policy
|
|
925
|
+
(2026-09-24).`);
|
|
926
|
+
const salesOrders = sales
|
|
927
|
+
.command("orders")
|
|
928
|
+
.description("quotations and sales orders (sale.order)");
|
|
929
|
+
salesOrders
|
|
930
|
+
.command("list")
|
|
931
|
+
.description("search and list orders with curated default columns")
|
|
932
|
+
.option("--partner <name|id>", "filter by customer (resolved by name)")
|
|
933
|
+
.option("--state <state>", "filter by state: draft, sent, sale, cancel")
|
|
934
|
+
.option("--filter <f=v>", "equality filter; repeatable, AND-ed", collect, [])
|
|
935
|
+
.option("--domain <json>", "Odoo domain as a JSON array (mutually exclusive with --filter)")
|
|
936
|
+
.option("--fields <a,b,c>", "columns to display")
|
|
937
|
+
.option("--limit <n>", "max records (default: 50)", "50")
|
|
938
|
+
.option("--offset <n>", "skip the first n records", "0")
|
|
939
|
+
.option("--order <order>", 'sort order, e.g. "amount_total desc"', undefined)
|
|
940
|
+
.action(async (opts) => {
|
|
941
|
+
await run((g) => ordersListCommand(opts, g), program);
|
|
942
|
+
});
|
|
943
|
+
salesOrders
|
|
944
|
+
.command("read")
|
|
945
|
+
.description("read one order in full, including order lines and partner details")
|
|
946
|
+
.argument("<id>", "order id")
|
|
947
|
+
.action(async (id) => {
|
|
948
|
+
await run((g) => ordersReadCommand(id, g), program);
|
|
949
|
+
});
|
|
950
|
+
salesOrders
|
|
951
|
+
.command("create")
|
|
952
|
+
.description("create a quotation (preview with --dry-run)")
|
|
953
|
+
.requiredOption("--partner <id|name>", "customer (res.partner id, or name)")
|
|
954
|
+
.option("--pricelist <id|name>", "pricelist (product.pricelist)")
|
|
955
|
+
.option("--payment-terms <id|name>", "payment terms (account.payment.term)")
|
|
956
|
+
.option("--validity <yyyy-mm-dd>", "quotation validity date")
|
|
957
|
+
.option("--opportunity <id|name>", "linked opportunity (crm.lead)")
|
|
958
|
+
.option("--salesperson <id|email|name>", "salesperson (res.users)")
|
|
959
|
+
.option("--plan <id|name>", "subscription plan (sale.subscription.plan)")
|
|
960
|
+
.option("--note <html>", "terms & conditions note (HTML allowed)")
|
|
961
|
+
.option("--lines <file>", "order lines as a JSON array from a file ('-' for stdin)")
|
|
962
|
+
.addHelpText("after", `
|
|
963
|
+
Line JSON format: an array of objects —
|
|
964
|
+
{"display_type":"line_section","name":"Section title"}
|
|
965
|
+
{"display_type":"line_note","name":"Note text"}
|
|
966
|
+
{"product_id":188,"product_uom_qty":1,"price_unit":150,"tax_ids":[37]}
|
|
967
|
+
On product lines, name defaults to the product name and price_unit to the
|
|
968
|
+
product list price; product_uom_qty defaults to 1.
|
|
969
|
+
|
|
970
|
+
Examples:
|
|
971
|
+
$ odoo sales orders create --partner 17682 --lines lines.json --dry-run
|
|
972
|
+
$ odoo sales orders create --partner "Acme AB" --pricelist "SEK price list" \\
|
|
973
|
+
--payment-terms "30 Days" --validity 2026-10-24 --plan Monthly --lines -`)
|
|
974
|
+
.action(async (opts) => {
|
|
975
|
+
await run((g) => ordersCreateCommand(opts, g), program);
|
|
976
|
+
});
|
|
977
|
+
salesOrders
|
|
978
|
+
.command("update")
|
|
979
|
+
.description("update order header fields (preview with --dry-run; state is NOT settable)")
|
|
980
|
+
.argument("<id>", "order id")
|
|
981
|
+
.option("--set <f=v>", "set a header field (plan_id, pricelist_id, payment_term_id, validity_date, note, opportunity_id, user_id); repeatable", collect, [])
|
|
982
|
+
.addHelpText("after", `
|
|
983
|
+
State changes (confirm/cancel) are deliberately NOT possible here — they
|
|
984
|
+
happen in the Odoo UI by a human. Sending quotations is likewise a human
|
|
985
|
+
step ('Send by Email' in the UI). Order lines are managed via 'sales lines'.
|
|
986
|
+
|
|
987
|
+
Examples:
|
|
988
|
+
$ odoo sales orders update 11515 --set plan_id=1 --dry-run
|
|
989
|
+
$ odoo sales orders update 11515 --set validity_date=2026-10-24 --set note="<p>Updated</p>"`)
|
|
990
|
+
.action(async (id, opts) => {
|
|
991
|
+
await run((g) => ordersUpdateCommand(id, opts, g), program);
|
|
992
|
+
});
|
|
993
|
+
const salesLines = sales
|
|
994
|
+
.command("lines")
|
|
995
|
+
.description("order lines (sale.order.line)");
|
|
996
|
+
salesLines
|
|
997
|
+
.command("add")
|
|
998
|
+
.description("add order lines to an order (preview with --dry-run)")
|
|
999
|
+
.argument("<order-id>", "sale.order id")
|
|
1000
|
+
.requiredOption("--lines <file>", "order lines as a JSON array from a file ('-' for stdin)")
|
|
1001
|
+
.addHelpText("after", `
|
|
1002
|
+
Same line JSON format as 'sales orders create'.
|
|
1003
|
+
|
|
1004
|
+
Examples:
|
|
1005
|
+
$ odoo sales lines add 11515 --lines extra-lines.json --dry-run
|
|
1006
|
+
$ echo '[{"product_id":188,"product_uom_qty":1,"price_unit":150,"tax_ids":[37]}]' | odoo sales lines add 11515 --lines -`)
|
|
1007
|
+
.action(async (orderId, opts) => {
|
|
1008
|
+
await run((g) => linesAddCommand(orderId, opts, g), program);
|
|
1009
|
+
});
|
|
1010
|
+
salesLines
|
|
1011
|
+
.command("update")
|
|
1012
|
+
.description("update one order line (preview with --dry-run)")
|
|
1013
|
+
.argument("<line-id>", "sale.order.line id")
|
|
1014
|
+
.requiredOption("--lines <file>", "single line object as JSON from a file ('-' for stdin)")
|
|
1015
|
+
.addHelpText("after", `
|
|
1016
|
+
The JSON must contain exactly one line object; only the keys present are
|
|
1017
|
+
updated (display_type, name, product_id, product_uom_qty, price_unit,
|
|
1018
|
+
tax_ids).
|
|
1019
|
+
|
|
1020
|
+
Examples:
|
|
1021
|
+
$ odoo sales lines update 20123 --lines line-fix.json --dry-run
|
|
1022
|
+
$ echo '[{"price_unit":175}]' | odoo sales lines update 20123 --lines -`)
|
|
1023
|
+
.action(async (lineId, opts) => {
|
|
1024
|
+
await run((g) => linesUpdateCommand(lineId, opts, g), program);
|
|
1025
|
+
});
|
|
1026
|
+
const salesPlans = sales
|
|
1027
|
+
.command("plans")
|
|
1028
|
+
.description("subscription plans (sale.subscription.plan)");
|
|
1029
|
+
salesPlans
|
|
1030
|
+
.command("list")
|
|
1031
|
+
.description("list subscription plans")
|
|
1032
|
+
.option("--limit <n>", "max records (default: 50)", "50")
|
|
1033
|
+
.action(async (opts) => {
|
|
1034
|
+
await run((g) => plansListCommand(opts, g), program);
|
|
1035
|
+
});
|
|
1036
|
+
const salesInvoices = sales
|
|
1037
|
+
.command("invoices")
|
|
1038
|
+
.description("invoicing reports (account.move.line)");
|
|
1039
|
+
salesInvoices
|
|
1040
|
+
.command("precedent")
|
|
1041
|
+
.description("posted customer-invoice lines for a product (newest first) — invoicing currency precedent")
|
|
1042
|
+
.requiredOption("--product <id|name>", "product (product.product)")
|
|
1043
|
+
.option("--limit <n>", "max records (default: 20)", "20")
|
|
1044
|
+
.addHelpText("after", `
|
|
1045
|
+
Searches account.move.line where the product matches and the parent move is
|
|
1046
|
+
a posted customer invoice (move_type=out_invoice, state=posted). Use it to
|
|
1047
|
+
determine the invoicing currency precedent for a product before quoting.
|
|
1048
|
+
|
|
1049
|
+
Examples:
|
|
1050
|
+
$ odoo sales invoices precedent --product 188
|
|
1051
|
+
$ odoo sales invoices precedent --product 188 --limit 5 --json`)
|
|
1052
|
+
.action(async (opts) => {
|
|
1053
|
+
await run((g) => invoicesPrecedentCommand(opts, g), program);
|
|
1054
|
+
});
|
|
1055
|
+
sales
|
|
1056
|
+
.command("pdf")
|
|
1057
|
+
.description("download the portal quotation PDF for an order")
|
|
1058
|
+
.argument("<id>", "order id")
|
|
1059
|
+
.requiredOption("--out <path>", "output path for the PDF")
|
|
1060
|
+
.option("--force", "overwrite an existing output file")
|
|
1061
|
+
.addHelpText("after", `
|
|
1062
|
+
Reads the order's access_token (falling back to get_portal_url) and
|
|
1063
|
+
downloads {base_url}/my/orders/<id>?access_token=<token>&report_type=pdf.
|
|
1064
|
+
The downloaded file is verified to start with %PDF.
|
|
1065
|
+
|
|
1066
|
+
Examples:
|
|
1067
|
+
$ odoo sales pdf 11515 --out ./quotation.pdf
|
|
1068
|
+
$ odoo sales pdf 11515 --out ./quotation.pdf --dry-run`)
|
|
1069
|
+
.action(async (id, opts) => {
|
|
1070
|
+
await run((g) => pdfCommand(id, opts, g), program);
|
|
1071
|
+
});
|
|
824
1072
|
const messages = program
|
|
825
1073
|
.command("messages")
|
|
826
1074
|
.description("chatter messages (mail.message), erasure-scoped");
|