quickbooks_types/models/
attachable.rs

1#[cfg(feature = "builder")]
2use std::path::Path;
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6use serde_with::skip_serializing_none;
7
8use super::common::{CustomField, MetaData, NtRef};
9use crate::{QBCreatable, QBDeletable, QBFullUpdatable, QBItem, QBToRef, QBTypeError};
10
11#[skip_serializing_none]
12#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
13#[serde(rename_all = "PascalCase", default)]
14#[cfg_attr(
15    feature = "builder",
16    derive(Builder),
17    builder(default, build_fn(error = "QBTypeError"), setter(into, strip_option))
18)]
19
20/// Attachable
21///
22/// Represents a file attachment or note that can be linked to other `QuickBooks` entities (for example: Invoice, Bill, Customer).
23///
24/// API reference:
25/// <https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/attachable>
26pub struct Attachable {
27    /// The unique ID of the entity
28    pub id: Option<String>,
29    /// The unique sync token of the entity, used for concurrency control
30    pub sync_token: Option<String>,
31    /// Metadata about the entity
32    #[serde(skip_serializing)]
33    pub meta_data: Option<MetaData>,
34    /// File name of the attachment
35    #[cfg_attr(feature = "builder", builder(setter(custom)))]
36    pub file_name: Option<String>,
37    #[cfg_attr(feature = "builder", builder(setter(custom)))]
38    #[serde(skip)]
39    pub file_path: Option<PathBuf>,
40    /// Private note for the attachment
41    pub note: Option<String>,
42    /// Category of the attachment
43    pub category: Option<AttachmentCategory>,
44    /// Content type of the attachment
45    #[cfg_attr(feature = "builder", builder(setter(custom)))]
46    pub content_type: Option<String>,
47    pub place_name: Option<String>,
48    /// References to the transaction object to which this attachable file is to be linked
49    #[cfg_attr(feature = "builder", builder(setter(strip_option)))]
50    pub attachable_ref: Option<Vec<AttachableRef>>,
51    /// Longitude of the place where the attachment was taken
52    pub long: Option<String>,
53    /// Tag for the attachment
54    pub tag: Option<String>,
55    /// Latitude of the place where the attachment was taken
56    pub lat: Option<String>,
57    /// URI for accessing the file
58    pub file_access_uri: Option<String>,
59    /// Size of the file in bytes
60    pub size: Option<f64>,
61    /// URI for accessing the thumbnail of the file
62    pub thumbnail_file_access_uri: Option<String>,
63    /// Temporary download URI for the file
64    pub temp_download_uri: Option<String>,
65}
66
67/// Derives the content type from a file extension
68#[must_use] 
69pub fn content_type_from_ext(ext: &str) -> Option<&'static str> {
70    let out = match ext {
71        "ai" | "eps" => "application/postscript",
72        "csv" => "text/csv",
73        "doc" => "application/msword",
74        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
75        "gif" => "image/gif",
76        "jpeg" => "image/jpeg",
77        "jpg" => "image/jpg",
78        "png" => "image/png",
79        "rtf" => "text/rtf",
80        "txt" => "text/plain",
81        "tif" => "image/tiff",
82        "ods" => "application/vnd.oasis.opendocument.spreadsheet",
83        "pdf" => "application/pdf",
84        "xls" => "application/vnd.ms-excel",
85        "xml" => "text/xml",
86        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
87        _ => return None,
88    };
89    Some(out)
90}
91
92#[cfg(feature = "builder")]
93impl AttachableBuilder {
94    /// Sets the file_path (for_upload) and derives the file_name and content_type from it
95    pub fn file(&mut self, path: &impl AsRef<Path>) -> Result<&mut Self, QBTypeError> {
96        let path = path.as_ref();
97
98        self.file_path = Some(Some(path.to_path_buf()));
99
100        self.file_name = Some(Some(
101            path.file_name()
102                .ok_or(QBTypeError::ValidationError(
103                    "No file name on file path".into(),
104                ))?
105                .to_str()
106                .ok_or(QBTypeError::ValidationError(
107                    "Could not turn file name into string".into(),
108                ))?
109                .to_owned(),
110        ));
111
112        self.content_type = Some(
113            content_type_from_ext(
114                path.extension()
115                    .ok_or(QBTypeError::ValidationError(
116                        "No extension on file/dir".into(),
117                    ))?
118                    .to_string_lossy()
119                    .as_ref(),
120            )
121            .map(|f| f.to_owned()),
122        );
123
124        Ok(self)
125    }
126}
127
128/// Trait for all entities that can be attached as files/notes
129pub trait QBAttachable {
130    fn can_upload(&self) -> Result<(), QBTypeError>;
131    fn file_path(&self) -> Option<&String>;
132}
133impl QBAttachable for Attachable {
134    fn can_upload(&self) -> Result<(), QBTypeError> {
135        if self.note.is_none() && self.file_name.is_none() {
136            return Err(QBTypeError::MissingField("note"));
137        }
138        if self.file_name.is_none() {
139            return Err(QBTypeError::MissingField("file_name"));
140        }
141        Ok(())
142    }
143
144    fn file_path(&self) -> Option<&String> {
145        self.file_name.as_ref()
146    }
147}
148
149/// `AttachmentCategory`
150///
151/// Enumerates the category of an attachment (for example: Document, Image, Receipt).
152#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
153pub enum AttachmentCategory {
154    ContactPhoto,
155    Document,
156    Image,
157    Receipt,
158    Signature,
159    Sound,
160    #[default]
161    Other,
162}
163
164/// AttachableRef
165///
166/// A reference that links an attachment to a target `QuickBooks` entity (such as an Invoice line or a Bill).
167///
168/// Most callers will construct this via `QBToAttachableRef::to_attach_ref()` on an entity that implements `QBToRef`.
169#[skip_serializing_none]
170#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
171#[serde(rename_all = "PascalCase", default)]
172#[cfg_attr(feature = "builder", derive(Builder), builder(default))]
173pub struct AttachableRef {
174    /// Indicates if the entity should be included on send
175    pub include_on_send: Option<bool>,
176    /// Line information for the entity
177    pub line_info: Option<String>,
178    /// Indicates if the entity is a reference only
179    pub no_ref_only: Option<bool>,
180    /// Custom fields for the entity
181    pub custom_field: Option<Vec<CustomField>>,
182    /// Type of the entity
183    #[serde(rename = "type")]
184    pub ref_type: Option<String>,
185    /// Indicates if the entity is inactive
186    pub inactive: Option<bool>,
187    /// The unique ID of the entity
188    pub entity_ref: Option<NtRef>,
189}
190
191impl From<NtRef> for AttachableRef {
192    fn from(value: NtRef) -> Self {
193        AttachableRef {
194            entity_ref: Some(value),
195            ..Default::default()
196        }
197    }
198}
199
200/// Trait for entities that can be converted to a reference for an attachment.
201pub trait QBToAttachableRef: QBToRef {
202    fn to_attach_ref(&self) -> Result<AttachableRef, QBTypeError> {
203        let value = self.to_ref()?;
204        Ok(value.into())
205    }
206}
207
208impl<T: QBToRef> QBToAttachableRef for T {}
209
210impl QBCreatable for Attachable {
211    fn can_create(&self) -> bool {
212        self.file_name.is_some() || self.note.is_some()
213    }
214}
215impl QBDeletable for Attachable {}
216impl QBFullUpdatable for Attachable {
217    fn can_full_update(&self) -> bool {
218        self.has_read() && self.can_create()
219    }
220}