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>;
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)]
24pub struct Line {
29 #[serde(flatten)]
31 pub line_detail: LineDetail,
32 pub amount: Option<f64>,
34 pub description: Option<String>,
36 pub id: Option<String>,
38 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 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#[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
140pub 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#[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#[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#[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#[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#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
243#[serde(rename_all = "PascalCase", default)]
244pub struct SubTotalLineDetail {
245 pub item_ref: NtRef,
246}
247
248#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
252pub enum BillableStatus {
253 #[default]
254 Billable,
255 NotBillable,
256 HasBeenBilled,
257}
258
259#[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#[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#[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}