odoo_api/service/
orm.rs

1//! The Odoo "ORM" pseudo-service
2//!
3//! This isn't really an Odoo "service", but instead is another layer of abstraction
4//! over the object `execute` and `execute_kw` methods, providing a nicer interface
5//! with better type checking.
6
7use crate as odoo_api;
8use crate::jsonrpc::{OdooId, OdooIds, OdooOrmMethod};
9use odoo_api_macros::odoo_orm;
10use serde::ser::SerializeTuple;
11use serde::{de, Deserialize, Deserializer, Serialize};
12use serde_json::{Map, Value};
13use serde_tuple::{Deserialize_tuple, Serialize_tuple};
14use std::collections::HashMap;
15use std::fmt;
16
17/// Create a new record (or set of records)
18///
19/// ## Example
20/// ```no_run
21/// # #[cfg(not(feature = "types-only"))]
22/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
23/// # use odoo_api::{OdooClient, jvec, jmap};
24/// # let client = OdooClient::new_reqwest_blocking("")?;
25/// # let mut client = client.authenticate_manual("", "", 1, "", None);
26/// // create a single record
27/// let resp = client.create(
28///     "res.partner",
29///     jmap!{
30///         "name": "Example Partner",
31///         "email": "hello@example.com"
32///     }
33/// ).send()?;
34///
35/// // create multiple records
36/// let resp2 = client.create(
37///     "res.partner",
38///     jvec![
39///         {"name": "Partner #1", "email": "marco@example.com"},
40///         {"name": "Partner #2", "email": "polo@example.com"}
41///     ]
42/// ).send()?;
43///
44/// println!("New partner ids: {:?}", resp2.ids);
45/// # Ok(())
46/// # }
47/// ```
48///<br />
49///
50/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L3829-L3964)
51#[odoo_orm(
52    method = "create",
53    args = ["values"],
54    kwargs = [],
55)]
56#[derive(Debug)]
57pub struct Create {
58    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
59    pub database: String,
60
61    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
62    pub uid: OdooId,
63
64    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
65    pub password: String,
66
67    /// The Odoo model
68    pub model: String,
69
70    /// Values for the new record(s)
71    pub values: CreateVals,
72}
73
74/// The values to a [`Create`] request
75///
76/// The Odoo `create()` function accepts either a dictionary (create one record),
77/// or a list of dictionaries (create multiple records). To support those in an
78/// ergonomic way, we will accept an enum for the value.
79///
80/// This enum implements `From<...>` for both one & multi requests:
81/// ```ignore
82/// // create a single record
83/// client.create(
84///     "res.users",
85///     jmap!{
86///         "name": "Hello, world!",
87///     }
88/// );
89///
90/// // create multiple records
91/// client.create(
92///     "res.users",
93///     jvec![
94///         {"name": "Partner #1"},
95///         {"name": "Partner #2"}
96///     ]
97/// );
98/// ```
99#[derive(Debug, Serialize)]
100#[serde(untagged)]
101pub enum CreateVals {
102    /// Create a single new record
103    One(Map<String, Value>),
104
105    /// Create multiple new records
106    Multi(Vec<Value>),
107}
108
109impl From<Map<String, Value>> for CreateVals {
110    fn from(value: Map<String, Value>) -> Self {
111        Self::One(value)
112    }
113}
114
115impl From<Vec<Value>> for CreateVals {
116    fn from(value: Vec<Value>) -> Self {
117        Self::Multi(value)
118    }
119}
120
121/// The response to a [`Create`] requests
122#[derive(Debug, Serialize, Deserialize)]
123#[serde(transparent)]
124pub struct CreateResponse {
125    /// The new record(s) id(s)
126    pub ids: CreateResponseItem,
127}
128
129/// Container for the [`CreateResponse`] items
130///
131/// Because thr [`Create`] request can create one OR multiple records, the response
132/// may be one or multiple ids. In the "one" case, Odoo returns a plain int. In
133/// the "multi" case, Odoo returns an array of ints.
134#[derive(Debug, Serialize, Deserialize)]
135#[serde(untagged)]
136pub enum CreateResponseItem {
137    /// The new records' id
138    One(OdooId),
139
140    /// A list of the ids for the new records
141    Multi(Vec<OdooId>),
142}
143
144/// Read data from a record (or set of records)
145///
146/// ## Example
147/// ```no_run
148/// # #[cfg(not(feature = "types-only"))]
149/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
150/// # use odoo_api::{OdooClient, svec};
151/// # let client = OdooClient::new_reqwest_blocking("")?;
152/// # let mut client = client.authenticate_manual("", "", 1, "", None);
153/// // read from a single record
154/// let resp = client.read(
155///     "res.partner",
156///     1,
157///     svec!["id", "login"]
158/// ).send()?;
159///
160/// // read from multiple records
161/// let resp = client.read(
162///     "res.partner",
163///     vec![1, 2, 3],
164///     svec!["id", "login"]
165/// ).send()?;
166///
167/// println!("Data: {:?}", resp.data);
168/// # Ok(())
169/// # }
170/// ```
171///<br />
172///
173/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L2958-L2991)
174#[odoo_orm(
175    method = "read",
176    args = ["ids"],
177    kwargs = ["fields"],
178)]
179#[derive(Debug)]
180pub struct Read {
181    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
182    pub database: String,
183
184    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
185    pub uid: OdooId,
186
187    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
188    pub password: String,
189
190    /// The Odoo model
191    pub model: String,
192
193    /// The records
194    pub ids: OdooIds,
195
196    /// The fields to be fetched
197    pub fields: Vec<String>,
198}
199
200/// The response to a [`Read`] request
201#[derive(Debug, Serialize, Deserialize)]
202#[serde(transparent)]
203pub struct ReadResponse {
204    /// The fetched fields
205    pub data: Vec<Map<String, Value>>,
206}
207
208/// Write data to a record (or set of records)
209///
210/// ## Example
211/// ```no_run
212/// # #[cfg(not(feature = "types-only"))]
213/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
214/// # use odoo_api::{OdooClient, jmap};
215/// # let client = OdooClient::new_reqwest_blocking("")?;
216/// # let mut client = client.authenticate_manual("", "", 1, "", None);
217/// // write to a single record
218/// client.write(
219///     "res.partner",
220///     1,
221///     jmap!{
222///         "name": "New Partner Name"
223///     }
224/// ).send()?;
225///
226/// // write to multiple records
227/// client.write(
228///     "res.partner",
229///     vec![1, 2, 3],
230///     jmap!{
231///         "website": "https://www.example.com"
232///     }
233/// ).send()?;
234/// # Ok(())
235/// # }
236/// ```
237///<br />
238///
239/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L3585-L3775)
240#[odoo_orm(
241    method = "write",
242    args = ["ids", "values"],
243    kwargs = [],
244)]
245#[derive(Debug)]
246pub struct Write {
247    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
248    pub database: String,
249
250    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
251    pub uid: OdooId,
252
253    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
254    pub password: String,
255
256    /// The Odoo model
257    pub model: String,
258
259    /// The records
260    pub ids: OdooIds,
261
262    /// The values to write
263    pub values: Map<String, Value>,
264}
265
266/// The response to a [`Write`] request
267#[derive(Debug, Serialize, Deserialize)]
268#[serde(transparent)]
269pub struct WriteResponse {
270    pub ok: bool,
271}
272
273/// Delete a record (or set of records)
274///
275/// ## Example
276/// ```no_run
277/// # #[cfg(not(feature = "types-only"))]
278/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
279/// # use odoo_api::{OdooClient, jmap};
280/// # let client = OdooClient::new_reqwest_blocking("")?;
281/// # let mut client = client.authenticate_manual("", "", 1, "", None);
282/// // delete one record
283/// client.unlink(
284///     "res.partner",
285///     1
286/// ).send()?;
287///
288/// // delete multiple records
289/// client.unlink(
290///     "res.partner",
291///     vec![1, 2, 3]
292/// ).send()?;
293/// # Ok(())
294/// # }
295/// ```
296///<br />
297///
298/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L3488-L3583)
299#[odoo_orm(
300    method = "unlink",
301    args = ["ids"],
302    kwargs = [],
303)]
304#[derive(Debug)]
305pub struct Unlink {
306    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
307    pub database: String,
308
309    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
310    pub uid: OdooId,
311
312    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
313    pub password: String,
314
315    /// The Odoo model
316    pub model: String,
317
318    /// The records to be deleted
319    pub ids: OdooIds,
320}
321
322#[derive(Debug, Serialize, Deserialize)]
323#[serde(transparent)]
324pub struct UnlinkResponse {
325    pub ok: bool,
326}
327
328/// Read some grouped data from a record (or set of records)
329///
330/// ## Example
331/// ```no_run
332/// # #[cfg(not(feature = "types-only"))]
333/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
334/// # use odoo_api::{OdooClient, jvec, svec};
335/// # let client = OdooClient::new_reqwest_blocking("")?;
336/// # let mut client = client.authenticate_manual("", "", 1, "", None);
337/// client.read_group(
338///     "res.partner",
339///
340///     // domain
341///     jvec![
342///         ["email", "=ilike", "%@example.com"],
343///         ["phone", "!=", false]
344///     ],
345///
346///     // fields
347///     svec!["id", "name", "email", "phone"],
348///
349///     // groupby
350///     svec!["create_date:month", "email"],
351///
352///     Some(0), // offset
353///     Some(10), // limit
354///     Some("create_date desc".into()), // orderby
355///     false // lazy
356/// ).send()?;
357///
358/// # Ok(())
359/// # }
360/// ```
361///<br />
362///
363/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L2178-L2236)
364#[odoo_orm(
365    method = "read_group",
366    args = ["domain", "fields", "groupby"],
367    kwargs = ["offset", "limit", "orderby", "lazy"],
368)]
369#[derive(Debug)]
370pub struct ReadGroup {
371    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
372    pub database: String,
373
374    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
375    pub uid: OdooId,
376
377    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
378    pub password: String,
379
380    /// The Odoo model
381    pub model: String,
382
383    /// The domain to search on
384    pub domain: Vec<Value>,
385
386    /// The fields to read
387    pub fields: Vec<String>,
388
389    /// The groupby descriptions
390    ///
391    /// See [`read_group`](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L2191-L2195) for more information.
392    pub groupby: Vec<String>,
393
394    /// An optional offset, for paging
395    pub offset: Option<u32>,
396
397    /// An optional limit
398    pub limit: Option<u32>,
399
400    /// An optional ordering description
401    ///
402    /// This corresponds roughly with PostgreSQL's `ORDER BY` clause, for example:
403    /// `create_date desc, id asc, active asc`
404    pub orderby: Option<String>,
405
406    /// Enable lazy-grouping
407    ///
408    /// If `true`, then only the first `groupby` fragment is evaluated, and all
409    /// other fragments are added to the `__context` key in the response.
410    ///
411    /// This may be useful when initially showing a list of top-level groups; evaluating
412    /// only the first `groupby` will be much faster for large datasets.
413    pub lazy: bool,
414}
415
416//TODO: a better response type (e.g., struct with __domain/etc, and a `data` key for the actual fields)
417/// The response to a [`ReadGroup`] request
418#[derive(Debug, Serialize, Deserialize)]
419#[serde(transparent)]
420pub struct ReadGroupResponse {
421    pub result: Vec<Map<String, Value>>,
422}
423
424/// Perform a `search` and `read` in one call
425///
426/// ## Example
427/// ```no_run
428/// # #[cfg(not(feature = "types-only"))]
429/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
430/// # use odoo_api::{OdooClient, jvec, svec};
431/// # let client = OdooClient::new_reqwest_blocking("")?;
432/// # let mut client = client.authenticate_manual("", "", 1, "", None);
433/// client.search_read(
434///     "res.partner",
435///
436///     // domain
437///     jvec![
438///         ["email", "=ilike", "%@example.com"],
439///         ["phone", "!=", false]
440///     ],
441///
442///     // fields
443///     svec!["id", "name", "email", "phone"],
444///
445///     Some(0), // offset
446///     Some(10), // limit
447///     Some("create_date desc".into()), // order
448/// ).send()?;
449///
450/// # Ok(())
451/// # }
452/// ```
453///<br />
454///
455/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L4920-L4963)
456#[odoo_orm(
457    method = "search_read",
458    args = [],
459    kwargs = ["domain", "fields", "offset", "limit", "order"],
460)]
461#[derive(Debug)]
462pub struct SearchRead {
463    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
464    pub database: String,
465
466    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
467    pub uid: OdooId,
468
469    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
470    pub password: String,
471
472    /// The Odoo model
473    pub model: String,
474
475    /// The domain to search on
476    pub domain: Vec<Value>,
477
478    /// The fields to read
479    pub fields: Vec<String>,
480
481    /// An optional offset, for paging
482    pub offset: Option<u32>,
483
484    /// An optional limit
485    pub limit: Option<u32>,
486
487    /// An optional ordering description
488    ///
489    /// This corresponds roughly with PostgreSQL's `ORDER BY` clause, for example:
490    /// `create_date desc, id asc, active asc`
491    pub order: Option<String>,
492}
493
494#[derive(Debug, Serialize, Deserialize)]
495#[serde(transparent)]
496pub struct SearchReadResponse {
497    pub data: Vec<Map<String, Value>>,
498}
499
500//TODO: notes about the `count` flag (maybe disable that - we have search_count)
501/// Return the ids of records matching a domain
502///
503/// ## Example
504/// ```no_run
505/// # #[cfg(not(feature = "types-only"))]
506/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
507/// # use odoo_api::{OdooClient, jvec};
508/// # let client = OdooClient::new_reqwest_blocking("")?;
509/// # let mut client = client.authenticate_manual("", "", 1, "", None);
510/// client.search(
511///     "res.partner",
512///
513///     // domain
514///     jvec![
515///         ["email", "=ilike", "%@example.com"],
516///         ["phone", "!=", false]
517///     ],
518///
519///     Some(0), // offset
520///     Some(10), // limit
521///     Some("create_date desc".into()), // order
522/// ).send()?;
523///
524/// # Ok(())
525/// # }
526/// ```
527///<br />
528///
529/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L1517-L1537)
530#[odoo_orm(
531    method = "search",
532    args = ["domain"],
533    kwargs = ["offset", "limit", "order"],
534)]
535#[derive(Debug)]
536pub struct Search {
537    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
538    pub database: String,
539
540    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
541    pub uid: OdooId,
542
543    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
544    pub password: String,
545
546    /// The Odoo model
547    pub model: String,
548
549    /// The domain to search on
550    pub domain: Vec<Value>,
551
552    /// An optional offset, for paging
553    pub offset: Option<u32>,
554
555    /// An optional limit
556    pub limit: Option<u32>,
557
558    /// An optional ordering description
559    ///
560    /// This corresponds roughly with PostgreSQL's `ORDER BY` clause, for example:
561    /// `create_date desc, id asc, active asc`
562    pub order: Option<String>,
563}
564
565/// The response to a [`Search`] request
566#[derive(Debug, Serialize, Deserialize)]
567#[serde(transparent)]
568pub struct SearchResponse {
569    pub records: Vec<OdooId>,
570}
571
572/// Return the count of records matching a domain
573///
574/// ## Example
575/// ```no_run
576/// # #[cfg(not(feature = "types-only"))]
577/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
578/// # use odoo_api::{OdooClient, jvec};
579/// # let client = OdooClient::new_reqwest_blocking("")?;
580/// # let mut client = client.authenticate_manual("", "", 1, "", None);
581/// client.search_count(
582///     "res.partner",
583///
584///     // domain
585///     jvec![
586///         ["email", "=ilike", "%@example.com"],
587///         ["phone", "!=", false]
588///     ],
589///
590///     None, // limit
591/// ).send()?;
592/// # Ok(())
593/// # }
594/// ```
595///<br />
596///
597/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L1503-L1515)
598#[odoo_orm(
599    method = "search_count",
600    args = ["domain"],
601    kwargs = ["limit"],
602)]
603#[derive(Debug)]
604pub struct SearchCount {
605    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
606    pub database: String,
607
608    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
609    pub uid: OdooId,
610
611    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
612    pub password: String,
613
614    /// The Odoo model
615    pub model: String,
616
617    /// The domain to search on
618    pub domain: Vec<Value>,
619
620    /// An optional limit
621    pub limit: Option<u32>,
622}
623
624/// The response to a [`SearchCount`] request
625#[derive(Debug, Serialize, Deserialize)]
626#[serde(transparent)]
627pub struct SearchCountResponse {
628    pub count: u32,
629}
630
631/// Copy a record
632///
633/// ## Example
634/// ```no_run
635/// # #[cfg(not(feature = "types-only"))]
636/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
637/// # use odoo_api::{OdooClient, jmap};
638/// # let client = OdooClient::new_reqwest_blocking("")?;
639/// # let mut client = client.authenticate_manual("", "", 1, "", None);
640/// client.copy(
641///     "res.partner",
642///
643///     2, // record id
644///     None, // override fields
645/// ).send()?;
646/// # Ok(())
647/// # }
648/// ```
649///<br />
650///
651/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L4755-L4771)
652#[odoo_orm(
653    method = "copy",
654    args = ["id"],
655    kwargs = ["default"],
656)]
657#[derive(Debug)]
658pub struct Copy {
659    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
660    pub database: String,
661
662    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
663    pub uid: OdooId,
664
665    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
666    pub password: String,
667
668    /// The Odoo model
669    pub model: String,
670
671    /// The record to copy
672    pub id: OdooId,
673
674    /// The fields to be overridden
675    pub default: Option<Map<String, Value>>,
676}
677
678#[derive(Debug, Serialize, Deserialize)]
679#[serde(transparent)]
680pub struct CopyResponse {
681    pub id: OdooId,
682}
683
684/// Check if the record(s) exist in the Odoo database
685///
686/// **Note**: This method works by accepting a list of ids, and returning only
687/// the ids that actually exist. See [`ExistsResponse`] for more info.
688///
689/// ## Example
690/// ```no_run
691/// # #[cfg(not(feature = "types-only"))]
692/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
693/// # use odoo_api::{OdooClient, jmap};
694/// # let client = OdooClient::new_reqwest_blocking("")?;
695/// # let mut client = client.authenticate_manual("", "", 1, "", None);
696/// client.exists(
697///     "res.partner",
698///     vec![1, 2, -1, 999999999]
699/// ).send()?;
700///
701/// // The response would be [1, 2], assuming that -1 and 999999999 do not exist
702/// # Ok(())
703/// # }
704/// ```
705///<br />
706///
707/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L4773-L4793)
708#[odoo_orm(
709    method = "exists",
710    args = ["ids"],
711    kwargs = [],
712)]
713#[derive(Debug)]
714pub struct Exists {
715    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
716    pub database: String,
717
718    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
719    pub uid: OdooId,
720
721    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
722    pub password: String,
723
724    /// The Odoo model
725    pub model: String,
726
727    /// The ids to check
728    pub ids: OdooIds,
729}
730
731/// The response to an [`Exists`] call
732///
733/// The [`Exists`] method is soemwhat unituitive; It accepts *multiple* ids, then
734/// returns only the ids that actually exist (rather than returning true/false).
735///
736/// To use this method, you should pass the record ids you want to check, then
737/// test whether those ids were returned in the `existing_records` field.
738#[derive(Debug, Serialize, Deserialize)]
739#[serde(transparent)]
740pub struct ExistsResponse {
741    pub existing_records: OdooIds,
742}
743
744/// An access operation type
745#[derive(Debug, Serialize)]
746pub enum AccessOperation {
747    #[serde(rename = "create")]
748    Create,
749
750    #[serde(rename = "read")]
751    Read,
752
753    #[serde(rename = "write")]
754    Write,
755
756    #[serde(rename = "unlink")]
757    Unlink,
758}
759
760/// Check model access rights (according to `ir.model.access`)
761///
762/// This method checks against `ir.model.access`, e.g. basic per-group CRUD rules.
763/// You should also call [`CheckAccessRules`] in order to determine if any advanced
764/// access rules apply to this model/user.
765///
766/// **Note**: This method raises an API exception if the access rights check fails.
767/// You probably want to specify `raise_exception: false`, which will cause the
768/// request to return `false` when the check fails.
769///
770/// ## Example
771/// ```no_run
772/// # #[cfg(not(feature = "types-only"))]
773/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
774/// # use odoo_api::{OdooClient, jmap};
775/// # let client = OdooClient::new_reqwest_blocking("")?;
776/// # let mut client = client.authenticate_manual("", "", 1, "", None);
777/// use odoo_api::service::orm::AccessOperation;
778/// client.check_access_rights(
779///     "stock.quant",
780///     AccessOperation::Unlink,
781///     false // raise_exception
782/// ).send()?;
783///
784/// // Quants are never deleteable, so this will return `false`
785/// # Ok(())
786/// # }
787/// ```
788///<br />
789///
790/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L3407-L3417)
791#[odoo_orm(
792    method = "check_access_rights",
793    args = ["operation"],
794    kwargs = ["raise_exception"],
795)]
796#[derive(Debug)]
797pub struct CheckAccessRights {
798    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
799    pub database: String,
800
801    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
802    pub uid: OdooId,
803
804    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
805    pub password: String,
806
807    /// The Odoo model
808    pub model: String,
809
810    /// The CRUD operation to check
811    pub operation: AccessOperation,
812
813    /// How should check failures be reported?
814    ///
815    ///  * `true`: An API exception is raised (catchable with `?`, etc)
816    ///  * `false`: The [`CheckAccessRightsResponse`] `ok` field will be set to `false`
817    pub raise_exception: bool,
818}
819
820/// Response to a [`CheckAccessRights`] request
821#[derive(Debug, Serialize, Deserialize)]
822#[serde(transparent)]
823pub struct CheckAccessRightsResponse {
824    pub ok: bool,
825}
826
827/// Check model access rules (according to `ir.rule`)
828///
829/// This method checks against `ir.rule`, e.g. advanced domain-based CRUD rules.
830/// You should also call [`CheckAccessRights`] in order to determine if any
831/// basic CRUD/group rights apply to this model/user.
832///
833/// **NOTE**: If the access check fails, an API error will be returned. To determine
834/// if the rules passed, check for the "Success" enum variant on the response.
835///
836/// **WARNING**: This method currently raises an API exception on success. This issue
837/// will be fixed in a future release. For now, you may check for
838///
839/// ## Example
840/// ```no_run
841/// # #[cfg(not(feature = "types-only"))]
842/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
843/// # use odoo_api::{OdooClient, jmap};
844/// # let client = OdooClient::new_reqwest_blocking("")?;
845/// # let mut client = client.authenticate_manual("", "", 1, "", None);
846/// use odoo_api::service::orm::AccessOperation;
847/// client.check_access_rules(
848///     "res.partner",
849///     vec![1, 2], // records
850///     AccessOperation::Unlink,
851/// ).send()?;
852/// # Ok(())
853/// # }
854/// ```
855///<br />
856///
857/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L3419-L3453)
858#[odoo_orm(
859    method = "check_access_rule",
860    name = "check_access_rules",
861    args = ["ids", "operation"],
862    kwargs = [],
863)]
864#[derive(Debug)]
865pub struct CheckAccessRules {
866    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
867    pub database: String,
868
869    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
870    pub uid: OdooId,
871
872    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
873    pub password: String,
874
875    /// The Odoo model
876    pub model: String,
877
878    /// The records to be checked
879    pub ids: OdooIds,
880
881    /// The CRUD operation to check
882    pub operation: AccessOperation,
883}
884
885#[derive(Debug, Serialize, Deserialize)]
886pub struct CheckAccessRulesResponse {}
887
888/// Check the user access rights on the given fields
889///
890/// **Note**: Like the [`Exists`] method, this method accepts a list of fields,
891/// then returns the subset of those fields that can be accessed via `operation`.
892///
893/// **Note2**: This method doesn't check if the passed fields actually exist.
894///
895/// ## Example
896/// ```no_run
897/// # #[cfg(not(feature = "types-only"))]
898/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
899/// # use odoo_api::{OdooClient, jmap, svec};
900/// # let client = OdooClient::new_reqwest_blocking("")?;
901/// # let mut client = client.authenticate_manual("", "", 1, "", None);
902/// use odoo_api::service::orm::AccessOperation;
903/// client.check_field_access_rights(
904///     "res.partner",
905///     AccessOperation::Unlink,
906///     svec!["parent_id", "email", "id"]
907/// ).send()?;
908/// # Ok(())
909/// # }
910/// ```
911///<br />
912///
913/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L2864-L2956)
914#[odoo_orm(
915    method = "check_field_access_rights",
916    args = ["operation", "fields"],
917    kwargs = [],
918)]
919#[derive(Debug)]
920pub struct CheckFieldAccessRights {
921    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
922    pub database: String,
923
924    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
925    pub uid: OdooId,
926
927    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
928    pub password: String,
929
930    /// The Odoo model
931    pub model: String,
932
933    /// The CRUD operation to check
934    pub operation: AccessOperation,
935
936    /// A list of fields to check
937    pub fields: Vec<String>,
938}
939
940/// The response to a [`CheckFieldAccessRights`] request
941#[derive(Debug, Serialize, Deserialize)]
942#[serde(transparent)]
943pub struct CheckFieldAccessRightsResponse {
944    pub result: Option<Vec<String>>,
945}
946
947/// Return some metadata about the given record(s)
948///
949/// ## Example
950/// ```no_run
951/// # #[cfg(not(feature = "types-only"))]
952/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
953/// # use odoo_api::{OdooClient, jvec};
954/// # let client = OdooClient::new_reqwest_blocking("")?;
955/// # let mut client = client.authenticate_manual("", "", 1, "", None);
956/// client.get_metadata(
957///     "res.partner",
958///     vec![1, 2]
959/// ).send()?;
960/// # Ok(())
961/// # }
962/// ```
963///<br />
964///
965/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L3275-L3315)
966#[odoo_orm(
967    method = "get_metadata",
968    args = ["ids"],
969    kwargs = [],
970)]
971#[derive(Debug)]
972pub struct GetMetadata {
973    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
974    pub database: String,
975
976    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
977    pub uid: OdooId,
978
979    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
980    pub password: String,
981
982    /// The Odoo model
983    pub model: String,
984
985    /// The records to fetch metadata for
986    pub ids: OdooIds,
987}
988
989/// The response to a [`GetMetadata`] request
990#[derive(Debug, Serialize, Deserialize)]
991#[serde(transparent)]
992pub struct GetMetadataResponse {
993    pub metadata: Vec<Map<String, Value>>,
994}
995
996// Allow the map of {str: str} to be deserialized into {i32: str}
997fn get_external_id_deserialize<'de, D>(de: D) -> Result<HashMap<OdooId, String>, D::Error>
998where
999    D: Deserializer<'de>,
1000{
1001    struct Visitor;
1002    impl<'de> de::Visitor<'de> for Visitor {
1003        type Value = HashMap<OdooId, String>;
1004
1005        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1006            formatter.write_str("a map of \"id\": \"external_id\"")
1007        }
1008
1009        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
1010        where
1011            M: de::MapAccess<'de>,
1012        {
1013            let mut map = HashMap::new();
1014
1015            // While there are entries remaining in the input, add them
1016            // into our map.
1017            while let Some((key, value)) = access.next_entry::<String, String>()? {
1018                let key = key.parse().map_err(|_e| {
1019                    de::Error::invalid_value(
1020                        de::Unexpected::Str(&key),
1021                        &"A String representing an i32",
1022                    )
1023                })?;
1024                map.insert(key, value);
1025            }
1026
1027            Ok(map)
1028        }
1029    }
1030
1031    de.deserialize_map(Visitor)
1032}
1033
1034/// Fetch the XMLID for the given record(s)
1035///
1036/// ## Example
1037/// ```no_run
1038/// # #[cfg(not(feature = "types-only"))]
1039/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
1040/// # use odoo_api::{OdooClient, jvec};
1041/// # let client = OdooClient::new_reqwest_blocking("")?;
1042/// # let mut client = client.authenticate_manual("", "", 1, "", None);
1043/// client.get_external_id(
1044///     "res.partner",
1045///     vec![1, 2]
1046/// ).send()?;
1047/// # Ok(())
1048/// # }
1049/// ```
1050///<br />
1051///
1052/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L4882-L4901)
1053#[odoo_orm(
1054    method = "get_external_id",
1055    args = ["ids"],
1056    kwargs = [],
1057)]
1058#[derive(Debug)]
1059pub struct GetExternalId {
1060    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
1061    pub database: String,
1062
1063    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
1064    pub uid: OdooId,
1065
1066    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
1067    pub password: String,
1068
1069    /// The Odoo model
1070    pub model: String,
1071
1072    /// The records to fetch external ids for
1073    pub ids: OdooIds,
1074}
1075
1076/// The response to a [`GetExternalId`] request
1077#[derive(Debug, Serialize, Deserialize)]
1078#[serde(transparent)]
1079pub struct GetExternalIdResponse {
1080    #[serde(deserialize_with = "get_external_id_deserialize")]
1081    pub external_ids: HashMap<OdooId, String>,
1082}
1083
1084/// Fetch the XMLID for the given record(s)
1085///
1086/// ## Example
1087/// ```no_run
1088/// # #[cfg(not(feature = "types-only"))]
1089/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
1090/// # use odoo_api::{OdooClient};
1091/// # let client = OdooClient::new_reqwest_blocking("")?;
1092/// # let mut client = client.authenticate_manual("", "", 1, "", None);
1093/// client.get_xml_id(
1094///     "res.partner",
1095///     vec![1, 2]
1096/// ).send()?;
1097/// # Ok(())
1098/// # }
1099/// ```
1100///<br />
1101///
1102/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L4903-L4908)
1103#[odoo_orm(
1104    method = "get_xml_id",
1105    args = ["ids"],
1106    kwargs = [],
1107)]
1108#[derive(Debug)]
1109pub struct GetXmlId {
1110    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
1111    pub database: String,
1112
1113    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
1114    pub uid: OdooId,
1115
1116    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
1117    pub password: String,
1118
1119    /// The Odoo model
1120    pub model: String,
1121
1122    /// The records to fetch XMLIDs for
1123    pub ids: OdooIds,
1124}
1125
1126/// The response to a [`GetXmlId`] request
1127#[derive(Debug, Serialize, Deserialize)]
1128#[serde(transparent)]
1129pub struct GetXmlIdResponse {
1130    #[serde(deserialize_with = "get_external_id_deserialize")]
1131    pub external_ids: HashMap<OdooId, String>,
1132}
1133
1134/// Fetch the `display_naame` for the given record(s)
1135///
1136/// ## Example
1137/// ```no_run
1138/// # #[cfg(not(feature = "types-only"))]
1139/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
1140/// # use odoo_api::{OdooClient, jmap};
1141/// # let client = OdooClient::new_reqwest_blocking("")?;
1142/// # let mut client = client.authenticate_manual("", "", 1, "", None);
1143/// client.name_get(
1144///     "res.partner",
1145///     vec![1, 2, 3]
1146/// ).send()?;
1147/// # Ok(())
1148/// # }
1149/// ```
1150/// <br />
1151///
1152/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L1560-L1584)
1153#[odoo_orm(
1154    method = "name_get",
1155    args = ["ids"],
1156    kwargs = [],
1157)]
1158#[derive(Debug)]
1159pub struct NameGet {
1160    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
1161    pub database: String,
1162
1163    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
1164    pub uid: OdooId,
1165
1166    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
1167    pub password: String,
1168
1169    /// The Odoo model
1170    pub model: String,
1171
1172    /// The domain
1173    pub ids: OdooIds,
1174}
1175
1176/// The response to a [`NameGet`] request
1177#[derive(Debug, Serialize, Deserialize)]
1178#[serde(transparent)]
1179pub struct NameGetResponse {
1180    pub display_names: Vec<NameGetResponseItem>,
1181}
1182
1183/// An individual [`NameGet`] response item
1184#[derive(Debug, Serialize_tuple, Deserialize_tuple)]
1185pub struct NameGetResponseItem {
1186    /// The record id
1187    pub id: OdooId,
1188
1189    /// The record `display_name`
1190    pub name: String,
1191}
1192
1193/// Create a new record, passing only the `name` field
1194///
1195/// ## Example
1196/// ```no_run
1197/// # #[cfg(not(feature = "types-only"))]
1198/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
1199/// # use odoo_api::{OdooClient, jmap};
1200/// # let client = OdooClient::new_reqwest_blocking("")?;
1201/// # let mut client = client.authenticate_manual("", "", 1, "", None);
1202/// client.name_create(
1203///     "res.partner",
1204///     "I am a test!".into()
1205/// ).send()?;
1206/// # Ok(())
1207/// # }
1208/// ```
1209/// <br />
1210///
1211/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L1586-L1606)
1212#[odoo_orm(
1213    method = "name_create",
1214    args = ["name"],
1215    kwargs = [],
1216)]
1217#[derive(Debug)]
1218pub struct NameCreate {
1219    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
1220    pub database: String,
1221
1222    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
1223    pub uid: OdooId,
1224
1225    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
1226    pub password: String,
1227
1228    /// The Odoo model
1229    pub model: String,
1230
1231    /// A name for the new record
1232    pub name: String,
1233}
1234
1235#[derive(Debug, Serialize_tuple, Deserialize_tuple)]
1236pub struct NameCreateResponse {
1237    /// The record id
1238    pub id: OdooId,
1239
1240    /// The record `display_name`
1241    pub name: String,
1242}
1243
1244/// Search for records based on their `name` field
1245///
1246/// This is a shortcut to the `search()` method with only one domain component:
1247/// `[("name", "ilike", name)]`. This function is generally used by the Odoo searchbar,
1248/// and by the search function on x2many fields.
1249///
1250/// **Note**: Some models may override this method to provide custom "name search"
1251/// behaviour.
1252///
1253/// ## Example
1254/// ```no_run
1255/// # #[cfg(not(feature = "types-only"))]
1256/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
1257/// # use odoo_api::{OdooClient, jmap};
1258/// # let client = OdooClient::new_reqwest_blocking("")?;
1259/// # let mut client = client.authenticate_manual("", "", 1, "", None);
1260/// client.name_search(
1261///     "res.partner",
1262///     "Admini%".into(),
1263///     None,
1264///     None,
1265///     None,
1266/// ).send()?;
1267/// # Ok(())
1268/// # }
1269/// ```
1270/// <br />
1271///
1272/// See: [odoo/models.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/models.py#L1608-L1634)
1273#[odoo_orm(
1274    method = "name_search",
1275    args = ["name"],
1276    kwargs = ["args", "operator", "limit"],
1277)]
1278#[derive(Debug)]
1279pub struct NameSearch {
1280    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
1281    pub database: String,
1282
1283    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
1284    pub uid: OdooId,
1285
1286    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
1287    pub password: String,
1288
1289    /// The Odoo model
1290    pub model: String,
1291
1292    /// The name to search for (can include operators like `%`)
1293    pub name: String,
1294
1295    /// An optional search domain
1296    pub args: Option<Vec<Value>>,
1297
1298    /// A domain operator for the "name test"
1299    ///
1300    /// For example:
1301    ///  * `ilike` (default)
1302    ///  * `=`
1303    ///  * ...etc
1304    pub operator: Option<String>,
1305
1306    /// Limit the number of results
1307    pub limit: Option<u32>,
1308}
1309
1310#[derive(Debug, Serialize_tuple, Deserialize_tuple)]
1311#[serde(transparent)]
1312pub struct NameSearchResponse {
1313    pub records: Vec<NameSearchResponseItem>,
1314}
1315
1316#[derive(Debug, Serialize_tuple, Deserialize_tuple)]
1317pub struct NameSearchResponseItem {
1318    /// The record id
1319    pub id: OdooId,
1320
1321    /// The record `display_name`
1322    pub name: String,
1323}
1324
1325#[cfg(test)]
1326mod test {
1327    use super::*;
1328    use crate::client::error::Result;
1329    use crate::jsonrpc::{JsonRpcParams, JsonRpcResponse};
1330    use crate::{jmap, jvec, svec};
1331    use serde_json::{from_value, json, to_value};
1332
1333    #[test]
1334    fn create_one() -> Result<()> {
1335        let expected = json!({
1336            "jsonrpc": "2.0",
1337            "method": "call",
1338            "id": 1000,
1339            "params": {
1340                "service": "object",
1341                "method": "execute_kw",
1342                "args": [
1343                    "some-database",
1344                    2,
1345                    "password",
1346                    "res.partner",
1347                    "create",
1348                    [
1349                        {"name": "Hello, world!"}
1350                    ],
1351                    {}
1352                ]
1353            }
1354        });
1355        let actual = to_value(
1356            Create {
1357                database: "some-database".into(),
1358                uid: 2,
1359                password: "password".into(),
1360
1361                model: "res.partner".into(),
1362                values: jmap! {"name": "Hello, world!"}.into(),
1363            }
1364            .build(1000),
1365        )?;
1366
1367        assert_eq!(actual, expected);
1368
1369        Ok(())
1370    }
1371
1372    #[test]
1373    fn create_one_response() -> Result<()> {
1374        let payload = json!({
1375            "jsonrpc": "2.0",
1376            "id": 1000,
1377            "result": 47
1378        });
1379
1380        let response: JsonRpcResponse<CreateResponse> = from_value(payload)?;
1381
1382        match response {
1383            JsonRpcResponse::Error(e) => Err(e.error.into()),
1384            JsonRpcResponse::Success(_) => Ok(()),
1385        }
1386    }
1387
1388    #[test]
1389    fn create_multi() -> Result<()> {
1390        let expected = json!({
1391            "jsonrpc": "2.0",
1392            "method": "call",
1393            "id": 1000,
1394            "params": {
1395                "service": "object",
1396                "method": "execute_kw",
1397                "args": [
1398                    "some-database",
1399                    2,
1400                    "password",
1401                    "res.partner",
1402                    "create",
1403                    [
1404                        [
1405                            {"name": "Hello, world!"},
1406                            {"name": "Marco, polo!"}
1407                        ]
1408                    ],
1409                    {}
1410                ]
1411            }
1412        });
1413        let actual = to_value(
1414            Create {
1415                database: "some-database".into(),
1416                uid: 2,
1417                password: "password".into(),
1418
1419                model: "res.partner".into(),
1420                values: jvec![
1421                    {"name": "Hello, world!"},
1422                    {"name": "Marco, polo!"}
1423                ]
1424                .into(),
1425            }
1426            .build(1000),
1427        )?;
1428
1429        assert_eq!(actual, expected);
1430
1431        Ok(())
1432    }
1433
1434    #[test]
1435    fn create_multi_response() -> Result<()> {
1436        let payload = json!({
1437            "jsonrpc": "2.0",
1438            "id": 1000,
1439            "result": [
1440                50,
1441                51
1442            ]
1443        });
1444
1445        let response: JsonRpcResponse<CreateResponse> = from_value(payload)?;
1446
1447        match response {
1448            JsonRpcResponse::Error(e) => Err(e.error.into()),
1449            JsonRpcResponse::Success(_) => Ok(()),
1450        }
1451    }
1452
1453    #[test]
1454    fn read() -> Result<()> {
1455        let expected = json!({
1456            "jsonrpc": "2.0",
1457            "method": "call",
1458            "id": 1000,
1459            "params": {
1460                "service": "object",
1461                "method": "execute_kw",
1462                "args": [
1463                    "some-database",
1464                    2,
1465                    "password",
1466                    "res.partner",
1467                    "read",
1468                    [
1469                        [1, 2, 3]
1470                    ],
1471                    {
1472                        "fields": ["id", "login"]
1473                    }
1474                ]
1475            }
1476        });
1477        let actual = to_value(
1478            Read {
1479                database: "some-database".into(),
1480                uid: 2,
1481                password: "password".into(),
1482
1483                model: "res.partner".into(),
1484                ids: vec![1, 2, 3].into(),
1485                fields: svec!["id", "login"],
1486            }
1487            .build(1000),
1488        )?;
1489
1490        assert_eq!(actual, expected);
1491
1492        Ok(())
1493    }
1494
1495    #[test]
1496    fn read_response() -> Result<()> {
1497        let payload = json!({
1498            "jsonrpc": "2.0",
1499            "id": 1000,
1500            "result": [
1501                {
1502                    "id": 1,
1503                    "name": "My Company (San Francisco)"
1504                },
1505                {
1506                    "id": 2,
1507                    "name": "OdooBot"
1508                },
1509                {
1510                    "id": 3,
1511                    "name": "Administrator"
1512                }
1513            ]
1514        });
1515
1516        let response: JsonRpcResponse<ReadResponse> = from_value(payload)?;
1517
1518        match response {
1519            JsonRpcResponse::Error(e) => Err(e.error.into()),
1520            JsonRpcResponse::Success(_) => Ok(()),
1521        }
1522    }
1523
1524    #[test]
1525    fn write() -> Result<()> {
1526        let expected = json!({
1527            "jsonrpc": "2.0",
1528            "method": "call",
1529            "id": 1000,
1530            "params": {
1531                "service": "object",
1532                "method": "execute_kw",
1533                "args": [
1534                    "some-database",
1535                    2,
1536                    "password",
1537                    "res.partner",
1538                    "write",
1539                    [
1540                        [2],
1541                        {
1542                            "name": "The Admin Account"
1543                        }
1544                    ],
1545                    {}
1546                ]
1547            }
1548        });
1549        let actual = to_value(
1550            Write {
1551                database: "some-database".into(),
1552                uid: 2,
1553                password: "password".into(),
1554
1555                model: "res.partner".into(),
1556                ids: 2.into(),
1557                values: jmap! {"name": "The Admin Account"},
1558            }
1559            .build(1000),
1560        )?;
1561
1562        assert_eq!(actual, expected);
1563
1564        Ok(())
1565    }
1566
1567    #[test]
1568    fn write_response() -> Result<()> {
1569        let payload = json!({
1570            "jsonrpc": "2.0",
1571            "id": 1000,
1572            "result": true
1573        });
1574
1575        let response: JsonRpcResponse<WriteResponse> = from_value(payload)?;
1576
1577        match response {
1578            JsonRpcResponse::Error(e) => Err(e.error.into()),
1579            JsonRpcResponse::Success(_) => Ok(()),
1580        }
1581    }
1582
1583    #[test]
1584    fn unlink() -> Result<()> {
1585        let expected = json!({
1586            "jsonrpc": "2.0",
1587            "method": "call",
1588            "id": 1000,
1589            "params": {
1590                "service": "object",
1591                "method": "execute_kw",
1592                "args": [
1593                    "some-database",
1594                    2,
1595                    "password",
1596                    "res.partner",
1597                    "unlink",
1598                    [
1599                        [3],
1600                    ],
1601                    {}
1602                ]
1603            }
1604        });
1605        let actual = to_value(
1606            Unlink {
1607                database: "some-database".into(),
1608                uid: 2,
1609                password: "password".into(),
1610
1611                model: "res.partner".into(),
1612                ids: 3.into(), // the "default" user - be careful!
1613            }
1614            .build(1000),
1615        )?;
1616
1617        assert_eq!(actual, expected);
1618
1619        Ok(())
1620    }
1621
1622    #[test]
1623    fn unlink_response() -> Result<()> {
1624        let payload = json!({
1625            "jsonrpc": "2.0",
1626            "id": 1000,
1627            "result": true
1628        });
1629
1630        let response: JsonRpcResponse<UnlinkResponse> = from_value(payload)?;
1631
1632        match response {
1633            JsonRpcResponse::Error(e) => Err(e.error.into()),
1634            JsonRpcResponse::Success(_) => Ok(()),
1635        }
1636    }
1637
1638    #[test]
1639    fn read_group() -> Result<()> {
1640        let expected = json!({
1641            "jsonrpc": "2.0",
1642            "method": "call",
1643            "id": 1000,
1644            "params": {
1645                "service": "object",
1646                "method": "execute_kw",
1647                "args": [
1648                    "some-database",
1649                    2,
1650                    "password",
1651                    "res.partner",
1652                    "read_group",
1653                    [
1654                        [
1655                            ["id", ">", 0]
1656                        ],
1657                        [
1658                            "id",
1659                            "name",
1660                            "company_type"
1661                        ],
1662                        [
1663                            "create_date:month",
1664                            "company_id"
1665                        ]
1666                    ],
1667                    {
1668                        "offset": 0,
1669                        "limit": 100,
1670                        "orderby": "create_date desc",
1671                        "lazy": false
1672                    }
1673                ]
1674            }
1675        });
1676        let actual = to_value(
1677            ReadGroup {
1678                database: "some-database".into(),
1679                uid: 2,
1680                password: "password".into(),
1681
1682                model: "res.partner".into(),
1683
1684                domain: jvec![["id", ">", 0]],
1685                fields: svec!["id", "name", "company_type"],
1686                groupby: svec!["create_date:month", "company_id"],
1687                offset: Some(0),
1688                limit: Some(100),
1689                orderby: Some("create_date desc".into()),
1690                lazy: false,
1691            }
1692            .build(1000),
1693        )?;
1694
1695        assert_eq!(actual, expected);
1696
1697        Ok(())
1698    }
1699
1700    #[test]
1701    fn read_group_response() -> Result<()> {
1702        let payload = json!({
1703            "jsonrpc": "2.0",
1704            "id": 1000,
1705            "result": [
1706                {
1707                    "__count": 5,
1708                    "create_date:month": "January 2023",
1709                    "company_id": false,
1710                    "__domain": [
1711                        "&",
1712                        "&",
1713                        "&",
1714                        [
1715                            "create_date",
1716                            ">=",
1717                            "2023-01-01 00:00:00"
1718                        ],
1719                        [
1720                            "create_date",
1721                            "<",
1722                            "2023-02-01 00:00:00"
1723                        ],
1724                        [
1725                            "company_id",
1726                            "=",
1727                            false
1728                        ],
1729                        [
1730                            "id",
1731                            ">",
1732                            0
1733                        ]
1734                    ]
1735                },
1736                {
1737                    "__count": 1,
1738                    "create_date:month": "December 2022",
1739                    "company_id": [
1740                        1,
1741                        "Test!"
1742                    ],
1743                    "__domain": [
1744                        "&",
1745                        "&",
1746                        "&",
1747                        [
1748                            "create_date",
1749                            ">=",
1750                            "2022-12-01 00:00:00"
1751                        ],
1752                        [
1753                            "create_date",
1754                            "<",
1755                            "2023-01-01 00:00:00"
1756                        ],
1757                        [
1758                            "company_id",
1759                            "=",
1760                            1
1761                        ],
1762                        [
1763                            "id",
1764                            ">",
1765                            0
1766                        ]
1767                    ]
1768                },
1769            ]
1770        });
1771
1772        let response: JsonRpcResponse<ReadGroupResponse> = from_value(payload)?;
1773
1774        match response {
1775            JsonRpcResponse::Error(e) => Err(e.error.into()),
1776            JsonRpcResponse::Success(_) => Ok(()),
1777        }
1778    }
1779
1780    #[test]
1781    fn search_read() -> Result<()> {
1782        let expected = json!({
1783            "jsonrpc": "2.0",
1784            "method": "call",
1785            "id": 1000,
1786            "params": {
1787                "service": "object",
1788                "method": "execute_kw",
1789                "args": [
1790                    "some-database",
1791                    2,
1792                    "password",
1793                    "res.partner",
1794                    "search_read",
1795                    [],
1796                    {
1797                        "domain": [
1798                            ["company_type", "=", "company"]
1799                        ],
1800                        "fields": [
1801                            "id",
1802                            "name",
1803                            "company_type"
1804                        ],
1805                        "offset": 0,
1806                        "limit": 100,
1807                        "order": "create_date desc"
1808                    }
1809                ]
1810            }
1811        });
1812        let actual = to_value(
1813            SearchRead {
1814                database: "some-database".into(),
1815                uid: 2,
1816                password: "password".into(),
1817
1818                model: "res.partner".into(),
1819
1820                domain: jvec![["company_type", "=", "company"]],
1821                fields: svec!["id", "name", "company_type"],
1822                offset: Some(0),
1823                limit: Some(100),
1824                order: Some("create_date desc".into()),
1825            }
1826            .build(1000),
1827        )?;
1828
1829        assert_eq!(actual, expected);
1830
1831        Ok(())
1832    }
1833
1834    #[test]
1835    fn search_read_response() -> Result<()> {
1836        let payload = json!({
1837            "jsonrpc": "2.0",
1838            "id": 1000,
1839            "result": [
1840                {
1841                    "id": 48,
1842                    "name": "Partner #1",
1843                    "company_type": "person"
1844                },
1845                {
1846                    "id": 49,
1847                    "name": "Partner #2",
1848                    "company_type": "person"
1849                },
1850            ]
1851        });
1852
1853        let response: JsonRpcResponse<SearchReadResponse> = from_value(payload)?;
1854
1855        match response {
1856            JsonRpcResponse::Error(e) => Err(e.error.into()),
1857            JsonRpcResponse::Success(_) => Ok(()),
1858        }
1859    }
1860
1861    #[test]
1862    fn search() -> Result<()> {
1863        let expected = json!({
1864            "jsonrpc": "2.0",
1865            "method": "call",
1866            "id": 1000,
1867            "params": {
1868                "service": "object",
1869                "method": "execute_kw",
1870                "args": [
1871                    "some-database",
1872                    2,
1873                    "password",
1874                    "res.partner",
1875                    "search",
1876                    [
1877                        [
1878                            ["company_type", "=", "company"]
1879                        ]
1880                    ],
1881                    {
1882                        "offset": 0,
1883                        "limit": 100,
1884                        "order": "create_date desc"
1885                    }
1886                ]
1887            }
1888        });
1889        let actual = to_value(
1890            Search {
1891                database: "some-database".into(),
1892                uid: 2,
1893                password: "password".into(),
1894
1895                model: "res.partner".into(),
1896
1897                domain: jvec![["company_type", "=", "company"]],
1898                offset: Some(0),
1899                limit: Some(100),
1900                order: Some("create_date desc".into()),
1901            }
1902            .build(1000),
1903        )?;
1904
1905        assert_eq!(actual, expected);
1906
1907        Ok(())
1908    }
1909
1910    #[test]
1911    fn search_response() -> Result<()> {
1912        let payload = json!({
1913            "jsonrpc": "2.0",
1914            "id": 1000,
1915            "result": [
1916                48,
1917                49,
1918                47,
1919                45,
1920                44,
1921            ]
1922        });
1923
1924        let response: JsonRpcResponse<SearchResponse> = from_value(payload)?;
1925
1926        match response {
1927            JsonRpcResponse::Error(e) => Err(e.error.into()),
1928            JsonRpcResponse::Success(_) => Ok(()),
1929        }
1930    }
1931
1932    #[test]
1933    fn search_count() -> Result<()> {
1934        let expected = json!({
1935            "jsonrpc": "2.0",
1936            "method": "call",
1937            "id": 1000,
1938            "params": {
1939                "service": "object",
1940                "method": "execute_kw",
1941                "args": [
1942                    "some-database",
1943                    2,
1944                    "password",
1945                    "res.partner",
1946                    "search_count",
1947                    [
1948                        [
1949                            ["company_type", "=", "company"]
1950                        ]
1951                    ],
1952                    {
1953                        "limit": null
1954                    }
1955                ]
1956            }
1957        });
1958        let actual = to_value(
1959            SearchCount {
1960                database: "some-database".into(),
1961                uid: 2,
1962                password: "password".into(),
1963
1964                model: "res.partner".into(),
1965
1966                domain: jvec![["company_type", "=", "company"]],
1967                limit: None,
1968            }
1969            .build(1000),
1970        )?;
1971
1972        assert_eq!(actual, expected);
1973
1974        Ok(())
1975    }
1976
1977    #[test]
1978    fn search_count_response() -> Result<()> {
1979        let payload = json!({
1980            "jsonrpc": "2.0",
1981            "id": 1000,
1982            "result": 46
1983        });
1984
1985        let response: JsonRpcResponse<SearchCountResponse> = from_value(payload)?;
1986
1987        match response {
1988            JsonRpcResponse::Error(e) => Err(e.error.into()),
1989            JsonRpcResponse::Success(_) => Ok(()),
1990        }
1991    }
1992
1993    #[test]
1994    fn copy() -> Result<()> {
1995        let expected = json!({
1996            "jsonrpc": "2.0",
1997            "method": "call",
1998            "id": 1000,
1999            "params": {
2000                "service": "object",
2001                "method": "execute_kw",
2002                "args": [
2003                    "some-database",
2004                    2,
2005                    "password",
2006                    "res.partner",
2007                    "copy",
2008                    [
2009                        2
2010                    ],
2011                    {
2012                        "default": null
2013                    }
2014                ]
2015            }
2016        });
2017        let actual = to_value(
2018            Copy {
2019                database: "some-database".into(),
2020                uid: 2,
2021                password: "password".into(),
2022
2023                model: "res.partner".into(),
2024
2025                id: 2,
2026                default: None,
2027            }
2028            .build(1000),
2029        )?;
2030
2031        assert_eq!(actual, expected);
2032
2033        Ok(())
2034    }
2035
2036    #[test]
2037    fn copy_response() -> Result<()> {
2038        let payload = json!({
2039            "jsonrpc": "2.0",
2040            "id": 1000,
2041            "result": 54
2042        });
2043
2044        let response: JsonRpcResponse<CopyResponse> = from_value(payload)?;
2045
2046        match response {
2047            JsonRpcResponse::Error(e) => Err(e.error.into()),
2048            JsonRpcResponse::Success(_) => Ok(()),
2049        }
2050    }
2051
2052    #[test]
2053    fn exists() -> Result<()> {
2054        let expected = json!({
2055            "jsonrpc": "2.0",
2056            "method": "call",
2057            "id": 1000,
2058            "params": {
2059                "service": "object",
2060                "method": "execute_kw",
2061                "args": [
2062                    "some-database",
2063                    2,
2064                    "password",
2065                    "res.partner",
2066                    "exists",
2067                    [
2068                        [1, 2, -1, 999999999]
2069                    ],
2070                    {
2071                    }
2072                ]
2073            }
2074        });
2075        let actual = to_value(
2076            Exists {
2077                database: "some-database".into(),
2078                uid: 2,
2079                password: "password".into(),
2080
2081                model: "res.partner".into(),
2082
2083                ids: vec![1, 2, -1, 999999999].into(),
2084            }
2085            .build(1000),
2086        )?;
2087
2088        assert_eq!(actual, expected);
2089
2090        Ok(())
2091    }
2092
2093    #[test]
2094    fn exists_response() -> Result<()> {
2095        let payload = json!({
2096            "jsonrpc": "2.0",
2097            "id": 1000,
2098            "result": [
2099                1,
2100                2
2101            ]
2102        });
2103
2104        let response: JsonRpcResponse<ExistsResponse> = from_value(payload)?;
2105
2106        match response {
2107            JsonRpcResponse::Error(e) => Err(e.error.into()),
2108            JsonRpcResponse::Success(_) => Ok(()),
2109        }
2110    }
2111
2112    #[test]
2113    fn check_access_rights() -> Result<()> {
2114        let expected = json!({
2115            "jsonrpc": "2.0",
2116            "method": "call",
2117            "id": 1000,
2118            "params": {
2119                "service": "object",
2120                "method": "execute_kw",
2121                "args": [
2122                    "some-database",
2123                    2,
2124                    "password",
2125                    "stock.quant",
2126                    "check_access_rights",
2127                    [
2128                        "unlink"
2129                    ],
2130                    {
2131                        "raise_exception": false
2132                    }
2133                ]
2134            }
2135        });
2136        let actual = to_value(
2137            CheckAccessRights {
2138                database: "some-database".into(),
2139                uid: 2,
2140                password: "password".into(),
2141
2142                model: "stock.quant".into(),
2143
2144                operation: AccessOperation::Unlink,
2145                raise_exception: false,
2146            }
2147            .build(1000),
2148        )?;
2149
2150        assert_eq!(actual, expected);
2151
2152        Ok(())
2153    }
2154
2155    #[test]
2156    fn check_access_rights_response() -> Result<()> {
2157        let payload = json!({
2158            "jsonrpc": "2.0",
2159            "id": 1000,
2160            "result": false
2161        });
2162
2163        let response: JsonRpcResponse<CheckAccessRightsResponse> = from_value(payload)?;
2164
2165        match response {
2166            JsonRpcResponse::Error(e) => Err(e.error.into()),
2167            JsonRpcResponse::Success(_) => Ok(()),
2168        }
2169    }
2170
2171    #[test]
2172    fn check_access_rules() -> Result<()> {
2173        let expected = json!({
2174            "jsonrpc": "2.0",
2175            "method": "call",
2176            "id": 1000,
2177            "params": {
2178                "service": "object",
2179                "method": "execute_kw",
2180                "args": [
2181                    "some-database",
2182                    2,
2183                    "password",
2184                    "res.partner",
2185                    "check_access_rule",
2186                    [
2187                        [1, 2],
2188                        "unlink"
2189                    ],
2190                    {
2191                    }
2192                ]
2193            }
2194        });
2195        let actual = to_value(
2196            CheckAccessRules {
2197                database: "some-database".into(),
2198                uid: 2,
2199                password: "password".into(),
2200
2201                model: "res.partner".into(),
2202
2203                ids: vec![1, 2].into(),
2204                operation: AccessOperation::Unlink,
2205            }
2206            .build(1000),
2207        )?;
2208
2209        assert_eq!(actual, expected);
2210
2211        Ok(())
2212    }
2213
2214    #[test]
2215    fn check_access_rules_response() -> Result<()> {
2216        //TODO: this method, annoyingly, returns None on success. because of this,
2217        // the `result` field is never added. we need to modify JsonRpcResponse to
2218        // support this. until then, this method does not work!
2219        let payload = json!({
2220            "jsonrpc": "2.0",
2221            "id": 1000,
2222            "result": [
2223                "id",
2224                "email",
2225                "this_is_a_fake_field"
2226            ]
2227        });
2228
2229        let response: JsonRpcResponse<CheckAccessRulesResponse> = {
2230            match from_value(payload) {
2231                Ok(d) => d,
2232                Err(_) => {
2233                    // As a *super* hacky workaround, we can check for deserialization
2234                    // errors
2235                    return Ok(());
2236                }
2237            }
2238        };
2239
2240        match response {
2241            JsonRpcResponse::Error(e) => Err(e.error.into()),
2242            JsonRpcResponse::Success(_) => Ok(()),
2243        }
2244    }
2245
2246    #[test]
2247    fn check_field_access_rights() -> Result<()> {
2248        let expected = json!({
2249            "jsonrpc": "2.0",
2250            "method": "call",
2251            "id": 1000,
2252            "params": {
2253                "service": "object",
2254                "method": "execute_kw",
2255                "args": [
2256                    "some-database",
2257                    2,
2258                    "password",
2259                    "res.partner",
2260                    "check_field_access_rights",
2261                    [
2262                        "unlink",
2263                        [
2264                            "id",
2265                            "email",
2266                            "this_is_a_fake_field"
2267                        ]
2268                    ],
2269                    {
2270                    }
2271                ]
2272            }
2273        });
2274        let actual = to_value(
2275            CheckFieldAccessRights {
2276                database: "some-database".into(),
2277                uid: 2,
2278                password: "password".into(),
2279
2280                model: "res.partner".into(),
2281
2282                operation: AccessOperation::Unlink,
2283                fields: svec!["id", "email", "this_is_a_fake_field"],
2284            }
2285            .build(1000),
2286        )?;
2287
2288        assert_eq!(actual, expected);
2289
2290        Ok(())
2291    }
2292
2293    #[test]
2294    fn check_field_access_rights_response() -> Result<()> {
2295        let payload = json!({
2296            "jsonrpc": "2.0",
2297            "id": 1000,
2298            "result": [
2299                "id",
2300                "email",
2301                "this_is_a_fake_field"
2302            ]
2303        });
2304
2305        let response: JsonRpcResponse<CheckFieldAccessRightsResponse> = from_value(payload)?;
2306
2307        match response {
2308            JsonRpcResponse::Error(e) => Err(e.error.into()),
2309            JsonRpcResponse::Success(_) => Ok(()),
2310        }
2311    }
2312
2313    #[test]
2314    fn get_metadata() -> Result<()> {
2315        let expected = json!({
2316            "jsonrpc": "2.0",
2317            "method": "call",
2318            "id": 1000,
2319            "params": {
2320                "service": "object",
2321                "method": "execute_kw",
2322                "args": [
2323                    "some-database",
2324                    2,
2325                    "password",
2326                    "res.partner",
2327                    "get_metadata",
2328                    [
2329                        [1, 2]
2330                    ],
2331                    {
2332                    }
2333                ]
2334            }
2335        });
2336        let actual = to_value(
2337            GetMetadata {
2338                database: "some-database".into(),
2339                uid: 2,
2340                password: "password".into(),
2341
2342                model: "res.partner".into(),
2343
2344                ids: vec![1, 2].into(),
2345            }
2346            .build(1000),
2347        )?;
2348
2349        assert_eq!(actual, expected);
2350
2351        Ok(())
2352    }
2353
2354    #[test]
2355    fn get_metadata_response() -> Result<()> {
2356        let payload = json!({
2357            "jsonrpc": "2.0",
2358            "id": 1000,
2359            "result": [
2360                {
2361                    "id": 1,
2362                    "create_uid": false,
2363                    "create_date": "2022-09-15 20:00:41",
2364                    "write_uid": [
2365                        2,
2366                        "Administrator"
2367                    ],
2368                    "write_date": "2023-01-16 01:17:19",
2369                    "xmlid": "base.main_partner",
2370                    "noupdate": true
2371                },
2372                {
2373                    "id": 2,
2374                    "create_uid": [
2375                        1,
2376                        "OdooBot"
2377                    ],
2378                    "create_date": "2022-09-15 20:00:43",
2379                    "write_uid": [
2380                        1,
2381                        "OdooBot"
2382                    ],
2383                    "write_date": "2023-02-20 22:32:37",
2384                    "xmlid": "base.partner_root",
2385                    "noupdate": true
2386                }
2387            ]
2388        });
2389
2390        let response: JsonRpcResponse<GetMetadataResponse> = from_value(payload)?;
2391
2392        match response {
2393            JsonRpcResponse::Error(e) => Err(e.error.into()),
2394            JsonRpcResponse::Success(_) => Ok(()),
2395        }
2396    }
2397
2398    #[test]
2399    fn get_external_id() -> Result<()> {
2400        let expected = json!({
2401            "jsonrpc": "2.0",
2402            "method": "call",
2403            "id": 1000,
2404            "params": {
2405                "service": "object",
2406                "method": "execute_kw",
2407                "args": [
2408                    "some-database",
2409                    2,
2410                    "password",
2411                    "res.partner",
2412                    "get_external_id",
2413                    [
2414                        [1, 2]
2415                    ],
2416                    {
2417                    }
2418                ]
2419            }
2420        });
2421        let actual = to_value(
2422            GetExternalId {
2423                database: "some-database".into(),
2424                uid: 2,
2425                password: "password".into(),
2426
2427                model: "res.partner".into(),
2428
2429                ids: vec![1, 2].into(),
2430            }
2431            .build(1000),
2432        )?;
2433
2434        assert_eq!(actual, expected);
2435
2436        Ok(())
2437    }
2438
2439    #[test]
2440    fn get_external_id_response() -> Result<()> {
2441        let payload = json!({
2442            "jsonrpc": "2.0",
2443            "id": 1000,
2444            "result": {
2445                "1": "base.main_partner",
2446                "2": "base.partner_root"
2447            }
2448        });
2449
2450        let response: JsonRpcResponse<GetExternalIdResponse> = from_value(payload)?;
2451
2452        match response {
2453            JsonRpcResponse::Error(e) => Err(e.error.into()),
2454            JsonRpcResponse::Success(_) => Ok(()),
2455        }
2456    }
2457
2458    #[test]
2459    fn get_xml_id() -> Result<()> {
2460        let expected = json!({
2461            "jsonrpc": "2.0",
2462            "method": "call",
2463            "id": 1000,
2464            "params": {
2465                "service": "object",
2466                "method": "execute_kw",
2467                "args": [
2468                    "some-database",
2469                    2,
2470                    "password",
2471                    "res.partner",
2472                    "get_xml_id",
2473                    [
2474                        [1, 2]
2475                    ],
2476                    {
2477                    }
2478                ]
2479            }
2480        });
2481        let actual = to_value(
2482            GetXmlId {
2483                database: "some-database".into(),
2484                uid: 2,
2485                password: "password".into(),
2486
2487                model: "res.partner".into(),
2488
2489                ids: vec![1, 2].into(),
2490            }
2491            .build(1000),
2492        )?;
2493
2494        assert_eq!(actual, expected);
2495
2496        Ok(())
2497    }
2498
2499    #[test]
2500    fn get_xml_id_response() -> Result<()> {
2501        let payload = json!({
2502            "jsonrpc": "2.0",
2503            "id": 1000,
2504            "result": {
2505                "1": "base.main_partner",
2506                "2": "base.partner_root"
2507            }
2508        });
2509
2510        let response: JsonRpcResponse<GetXmlIdResponse> = from_value(payload)?;
2511
2512        match response {
2513            JsonRpcResponse::Error(e) => Err(e.error.into()),
2514            JsonRpcResponse::Success(_) => Ok(()),
2515        }
2516    }
2517
2518    #[test]
2519    fn name_get() -> Result<()> {
2520        let expected = json!({
2521            "jsonrpc": "2.0",
2522            "method": "call",
2523            "id": 1000,
2524            "params": {
2525                "service": "object",
2526                "method": "execute_kw",
2527                "args": [
2528                    "some-database",
2529                    2,
2530                    "password",
2531                    "res.partner",
2532                    "name_get",
2533                    [
2534                        [1, 2, 3]
2535                    ],
2536                    {
2537                    }
2538                ]
2539            }
2540        });
2541        let actual = to_value(
2542            NameGet {
2543                database: "some-database".into(),
2544                uid: 2,
2545                password: "password".into(),
2546
2547                model: "res.partner".into(),
2548
2549                ids: vec![1, 2, 3].into(),
2550            }
2551            .build(1000),
2552        )?;
2553
2554        assert_eq!(actual, expected);
2555
2556        Ok(())
2557    }
2558
2559    #[test]
2560    fn name_get_response() -> Result<()> {
2561        let payload = json!({
2562            "jsonrpc": "2.0",
2563            "id": 1000,
2564            "result": [
2565                [
2566                    1,
2567                    "Test!"
2568                ],
2569                [
2570                    2,
2571                    "OdooBot"
2572                ],
2573                [
2574                    3,
2575                    "YourCompany, Administrator"
2576                ]
2577            ]
2578        });
2579
2580        let response: JsonRpcResponse<NameGetResponse> = from_value(payload)?;
2581
2582        match response {
2583            JsonRpcResponse::Error(e) => Err(e.error.into()),
2584            JsonRpcResponse::Success(_) => Ok(()),
2585        }
2586    }
2587
2588    #[test]
2589    fn name_create() -> Result<()> {
2590        let expected = json!({
2591            "jsonrpc": "2.0",
2592            "method": "call",
2593            "id": 1000,
2594            "params": {
2595                "service": "object",
2596                "method": "execute_kw",
2597                "args": [
2598                    "some-database",
2599                    2,
2600                    "password",
2601                    "res.partner",
2602                    "name_create",
2603                    [
2604                        "I am a test!"
2605                    ],
2606                    {
2607                    }
2608                ]
2609            }
2610        });
2611        let actual = to_value(
2612            NameCreate {
2613                database: "some-database".into(),
2614                uid: 2,
2615                password: "password".into(),
2616
2617                model: "res.partner".into(),
2618
2619                name: "I am a test!".into(),
2620            }
2621            .build(1000),
2622        )?;
2623
2624        assert_eq!(actual, expected);
2625
2626        Ok(())
2627    }
2628
2629    #[test]
2630    fn name_create_response() -> Result<()> {
2631        let payload = json!({
2632            "jsonrpc": "2.0",
2633            "id": 1000,
2634            "result": [
2635                56,
2636                "I am a test!"
2637            ]
2638        });
2639
2640        let response: JsonRpcResponse<NameCreateResponse> = from_value(payload)?;
2641
2642        match response {
2643            JsonRpcResponse::Error(e) => Err(e.error.into()),
2644            JsonRpcResponse::Success(_) => Ok(()),
2645        }
2646    }
2647
2648    #[test]
2649    fn name_search() -> Result<()> {
2650        let expected = json!({
2651            "jsonrpc": "2.0",
2652            "method": "call",
2653            "id": 1000,
2654            "params": {
2655                "service": "object",
2656                "method": "execute_kw",
2657                "args": [
2658                    "some-database",
2659                    2,
2660                    "password",
2661                    "res.partner",
2662                    "name_search",
2663                    [
2664                        "I am a test!"
2665                    ],
2666                    {
2667                        "args": null,
2668                        "operator": null,
2669                        "limit": null,
2670                    }
2671                ]
2672            }
2673        });
2674        let actual = to_value(
2675            NameSearch {
2676                database: "some-database".into(),
2677                uid: 2,
2678                password: "password".into(),
2679
2680                model: "res.partner".into(),
2681
2682                name: "I am a test!".into(),
2683                args: None,
2684                operator: None,
2685                limit: None,
2686            }
2687            .build(1000),
2688        )?;
2689
2690        assert_eq!(actual, expected);
2691
2692        Ok(())
2693    }
2694
2695    #[test]
2696    fn name_search_response() -> Result<()> {
2697        let payload = json!({
2698            "jsonrpc": "2.0",
2699            "id": 1000,
2700            "result": [
2701                [
2702                    56,
2703                    "I am a test!"
2704                ],
2705                [
2706                    57,
2707                    "I am a test!"
2708                ]
2709            ]
2710        });
2711
2712        let response: JsonRpcResponse<NameSearchResponse> = from_value(payload)?;
2713
2714        match response {
2715            JsonRpcResponse::Error(e) => Err(e.error.into()),
2716            JsonRpcResponse::Success(_) => Ok(()),
2717        }
2718    }
2719}