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