quickbooks_types/models/
line.rs

1use chrono::{DateTime, NaiveDate, Utc};
2use serde::{ser::SerializeStruct, Deserialize, Serialize};
3use serde_with::skip_serializing_none;
4
5use super::common::{LinkedTxn, NtRef};
6use crate::QBCreatable;
7
8/// `LineField`
9///
10/// A collection of Line items, typically used within transactions.
11pub type LineField = Vec<Line>;
12
13#[cfg(feature = "builder")]
14use crate::error::QBTypeError;
15
16#[skip_serializing_none]
17#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
18#[serde(rename_all = "PascalCase", default)]
19#[cfg_attr(
20    feature = "builder",
21    derive(Builder),
22    builder(default, build_fn(error = "QBTypeError"), setter(into, strip_option))
23)]
24/// Line
25///
26/// Represents a single line within a transaction (e.g., Invoice, Bill, `SalesReceipt`). Encapsulates amount, description, and a specific `LineDetail` subtype.
27/// Note: This type has no standalone `QuickBooks` API endpoint and is only used as a nested component.
28pub struct Line {
29    /// Details of the line item
30    #[serde(flatten)]
31    pub line_detail: LineDetail,
32    /// Amount total for the line item
33    pub amount: Option<f64>,
34    /// Description of the line item
35    pub description: Option<String>,
36    /// Unique line number
37    pub id: Option<String>,
38    /// Linked transactions
39    pub linked_txn: Option<Vec<LinkedTxn>>,
40}
41
42impl QBCreatable for Line {
43    fn can_create(&self) -> bool {
44        self.amount.is_some()
45    }
46}
47
48impl QBCreatable for Option<LineField> {
49    fn can_create(&self) -> bool {
50        if let Some(data) = self {
51            data.can_create()
52        } else {
53            false
54        }
55    }
56}
57
58impl QBCreatable for LineField {
59    fn can_create(&self) -> bool {
60        self.iter().all(QBCreatable::can_create)
61    }
62}
63
64impl Serialize for LineDetail {
65    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
66    where
67        S: serde::Serializer,
68    {
69        let mut state = serializer.serialize_struct("LineDetail", 2)?;
70
71        // TODO Make this more generic, although there won't be more types to add in the future most likely
72        let detail_type = match self {
73            LineDetail::SalesItemLineDetail(data) => {
74                state.serialize_field("SalesItemLineDetail", data)?;
75                "SalesItemLineDetail"
76            }
77            LineDetail::GroupLineDetail(data) => {
78                state.serialize_field("GroupLineDetail", data)?;
79                "GroupLineDetail"
80            }
81            LineDetail::DescriptionLineDetail(data) => {
82                state.serialize_field("DescriptionLineDetail", data)?;
83                "DescriptionLineDetail"
84            }
85            LineDetail::DiscountLineDetail(data) => {
86                state.serialize_field("DiscountLineDetail", data)?;
87                "DiscountLineDetail"
88            }
89            LineDetail::SubTotalLineDetail(data) => {
90                state.serialize_field("SubTotalLineDetail", data)?;
91                "SubTotalLineDetail"
92            }
93            LineDetail::ItemBasedExpenseLineDetail(data) => {
94                state.serialize_field("ItemBasedExpenseLineDetail", data)?;
95                "ItemBasedExpenseLineDetail"
96            }
97            LineDetail::AccountBasedExpenseLineDetail(data) => {
98                state.serialize_field("AccountBasedExpenseLineDetail", data)?;
99                "AccountBasedExpenseLineDetail"
100            }
101            LineDetail::TaxLineDetail(data) => {
102                state.serialize_field("TaxLineDetail", data)?;
103                "TaxLineDetail"
104            }
105            LineDetail::None => panic!("Cannot serialize Line Detail of None!"),
106        };
107
108        state.serialize_field("DetailType", detail_type)?;
109        state.end()
110    }
111}
112
113impl std::fmt::Display for Line {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(
116            f,
117            "{}",
118            serde_json::to_string_pretty(self).expect("Could not serialize Line for display!")
119        )
120    }
121}
122
123/// `LineDetail` Enum
124///
125/// Subtype of the line detail
126#[derive(Clone, Debug, Deserialize, PartialEq, Default)]
127pub enum LineDetail {
128    SalesItemLineDetail(SalesItemLineDetail),
129    GroupLineDetail(GroupLineDetail),
130    DescriptionLineDetail(DescriptionLineDetail),
131    DiscountLineDetail(DiscountLineDetail),
132    SubTotalLineDetail(SubTotalLineDetail),
133    ItemBasedExpenseLineDetail(ItemBasedExpenseLineDetail),
134    AccountBasedExpenseLineDetail(AccountBasedExpenseLineDetail),
135    TaxLineDetail(TaxLineDetail),
136    #[default]
137    None,
138}
139
140/// Trait for setting a line / line detail as taxable
141pub trait TaxableLine {
142    fn set_taxable(&mut self);
143}
144
145impl TaxableLine for LineDetail {
146    fn set_taxable(&mut self) {
147        if let LineDetail::SalesItemLineDetail(data) = self {
148            data.tax_code_ref = Some("TAX".into());
149        }
150    }
151}
152
153impl TaxableLine for Line {
154    fn set_taxable(&mut self) {
155        self.line_detail.set_taxable();
156    }
157}
158
159impl TaxableLine for LineField {
160    fn set_taxable(&mut self) {
161        self.iter_mut().for_each(TaxableLine::set_taxable);
162    }
163}
164
165impl TaxableLine for Option<LineField> {
166    fn set_taxable(&mut self) {
167        self.iter_mut().for_each(TaxableLine::set_taxable);
168    }
169}
170
171impl<T> TaxableLine for std::slice::IterMut<'_, T>
172where
173    T: TaxableLine,
174{
175    fn set_taxable(&mut self) {
176        self.for_each(TaxableLine::set_taxable);
177    }
178}
179
180/// `SalesItemLineDetail`
181///
182/// Description of the sales item line detail
183#[skip_serializing_none]
184#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
185#[serde(rename_all = "PascalCase", default)]
186#[cfg_attr(
187    feature = "builder",
188    derive(Builder),
189    builder(default, build_fn(error = "QBTypeError"), setter(into, strip_option))
190)]
191pub struct SalesItemLineDetail {
192    pub tax_inclusive_amt: Option<f64>,
193    pub discount_amt: Option<f64>,
194    pub item_ref: Option<NtRef>,
195    pub class_ref: Option<NtRef>,
196    pub tax_code_ref: Option<NtRef>,
197    pub service_date: Option<NaiveDate>,
198    pub discount_rate: Option<f64>,
199    pub qty: Option<f64>,
200    pub unit_price: Option<f64>,
201    pub tax_classification_ref: Option<NtRef>,
202}
203
204/// `GroupLineDetail`
205///
206/// Description of the group line detail
207#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
208#[serde(rename_all = "PascalCase", default)]
209pub struct GroupLineDetail {
210    pub quantity: f64,
211    pub line: LineField,
212    pub group_item_ref: NtRef,
213}
214
215/// `DescriptionLineDetail`
216///
217/// Description of the description line detail
218#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
219#[serde(rename_all = "PascalCase", default)]
220pub struct DescriptionLineDetail {
221    pub tax_code_ref: NtRef,
222    pub service_date: DateTime<Utc>,
223}
224
225/// `DiscountLineDetail`
226///
227/// Description of the discount line detail
228#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
229#[serde(rename_all = "PascalCase", default)]
230pub struct DiscountLineDetail {
231    pub class_ref: NtRef,
232    pub tax_code_ref: NtRef,
233    pub discount_account_ref: NtRef,
234    pub percent_based: bool,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub discount_percent: Option<f64>,
237}
238
239/// `SubTotalLineDetail`
240///
241/// Description of the subtotal line detail
242#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
243#[serde(rename_all = "PascalCase", default)]
244pub struct SubTotalLineDetail {
245    pub item_ref: NtRef,
246}
247
248/// `BillableStatus`
249///
250/// Indicates the billable status of an expense line item.
251#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
252pub enum BillableStatus {
253    #[default]
254    Billable,
255    NotBillable,
256    HasBeenBilled,
257}
258
259/// `ItemBasedExpenseLineDetail`
260///
261/// Description of the item-based expense line detail
262#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
263#[serde(rename_all = "PascalCase", default)]
264pub struct ItemBasedExpenseLineDetail {
265    pub tax_inclusive_amt: f64,
266    pub item_ref: NtRef,
267    pub customer_ref: NtRef,
268    pub price_level_ref: NtRef,
269    pub class_ref: NtRef,
270    pub tax_code_ref: NtRef,
271    pub billable_status: BillableStatus,
272    pub qty: f64,
273    pub unit_price: f64,
274}
275
276/// `AccountBasedExpenseLineDetail`
277///
278/// Description of the account-based expense line detail
279#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
280#[serde(default, rename_all = "PascalCase")]
281pub struct AccountBasedExpenseLineDetail {
282    pub account_ref: NtRef,
283    pub tax_code_ref: NtRef,
284    pub tax_amount: f64,
285    pub tax_inclusive_amt: f64,
286    pub class_ref: NtRef,
287    pub customer_ref: NtRef,
288    pub billable_status: BillableStatus,
289}
290
291/// `TaxLineDetail`
292///
293/// Description of the tax line detail
294#[skip_serializing_none]
295#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
296#[serde(rename_all = "PascalCase", default)]
297pub struct TaxLineDetail {
298    pub tax_rate_ref: Option<NtRef>,
299    pub net_amount_taxable: Option<f64>,
300    pub percent_based: Option<bool>,
301    pub tax_inclusive_amount: Option<f64>,
302    pub override_delta_amount: Option<f64>,
303    pub tax_percent: Option<f64>,
304}
305
306#[test]
307fn deserialize_line() {
308    let test: LineField = serde_json::from_str(
309        r#"[{
310      "Description": "Rock Fountain",
311      "DetailType": "SalesItemLineDetail",
312      "SalesItemLineDetail": {
313        "TaxCodeRef": {
314          "value": "TAX"
315        },
316        "Qty": 1,
317        "UnitPrice": 275,
318        "ItemRef": {
319          "name": "Rock Fountain",
320          "value": "5"
321        }
322      },
323      "LineNum": 1,
324      "Amount": 275.0,
325      "Id": "1"
326    },
327    {
328      "Description": "Fountain Pump",
329      "DetailType": "SalesItemLineDetail",
330      "SalesItemLineDetail": {
331        "TaxCodeRef": {
332          "value": "TAX"
333        },
334        "Qty": 1,
335        "UnitPrice": 12.75,
336        "ItemRef": {
337          "name": "Pump",
338          "value": "11"
339        }
340      },
341      "LineNum": 2,
342      "Amount": 12.75,
343      "Id": "2"
344    },
345    {
346      "Description": "Concrete for fountain installation",
347      "DetailType": "SalesItemLineDetail",
348      "SalesItemLineDetail": {
349        "TaxCodeRef": {
350          "value": "TAX"
351        },
352        "Qty": 5,
353        "UnitPrice": 9.5,
354        "ItemRef": {
355          "name": "Concrete",
356          "value": "3"
357        }
358      },
359      "LineNum": 3,
360      "Amount": 47.5,
361      "Id": "3"
362    },
363    {
364      "DetailType": "SubTotalLineDetail",
365      "Amount": 335.25,
366      "SubTotalLineDetail": {}
367    }
368  ]"#,
369    )
370    .unwrap();
371    dbg!(test);
372}