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)]
21pub struct Line {
26 #[serde(flatten)]
28 pub line_detail: LineDetail,
29 pub amount: Option<f64>,
31 pub description: Option<String>,
33 pub id: Option<String>,
35 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 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#[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
137pub 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#[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#[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#[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#[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#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
240#[serde(rename_all = "PascalCase", default)]
241pub struct SubTotalLineDetail {
242 pub item_ref: NtRef,
243}
244
245#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
249pub enum BillableStatus {
250 #[default]
251 Billable,
252 NotBillable,
253 HasBeenBilled,
254}
255
256#[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#[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#[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}