Skip to main content

backbone_inventory/domain/entity/
stock_move_line.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::MoveState;
8use super::AuditMetadata;
9
10/// Strongly-typed ID for StockMoveLine
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct StockMoveLineId(pub Uuid);
14
15impl StockMoveLineId {
16    pub fn new(id: Uuid) -> Self { Self(id) }
17    pub fn generate() -> Self { Self(Uuid::new_v4()) }
18    pub fn into_inner(self) -> Uuid { self.0 }
19}
20
21impl std::fmt::Display for StockMoveLineId {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "{}", self.0)
24    }
25}
26
27impl std::str::FromStr for StockMoveLineId {
28    type Err = uuid::Error;
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        Ok(Self(Uuid::parse_str(s)?))
31    }
32}
33
34impl From<Uuid> for StockMoveLineId {
35    fn from(id: Uuid) -> Self { Self(id) }
36}
37
38impl From<StockMoveLineId> for Uuid {
39    fn from(id: StockMoveLineId) -> Self { id.0 }
40}
41
42impl AsRef<Uuid> for StockMoveLineId {
43    fn as_ref(&self) -> &Uuid { &self.0 }
44}
45
46impl std::ops::Deref for StockMoveLineId {
47    type Target = Uuid;
48    fn deref(&self) -> &Self::Target { &self.0 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
52pub struct StockMoveLine {
53    pub id: Uuid,
54    pub quantity: Decimal,
55    pub quantity_product_uom_id: Option<Uuid>,
56    pub picked: bool,
57    pub lot_id: Option<Uuid>,
58    pub package_id: Option<Uuid>,
59    pub result_package_id: Option<Uuid>,
60    pub owner_id: Option<Uuid>,
61    pub state: MoveState,
62    pub date: DateTime<Utc>,
63    pub move_id: Uuid,
64    pub picking_id: Option<Uuid>,
65    pub location_id: Uuid,
66    pub location_dest_id: Uuid,
67    pub item_id: Uuid,
68    #[serde(default)]
69    #[sqlx(json)]
70    pub metadata: AuditMetadata,
71}
72
73impl StockMoveLine {
74    /// Create a builder for StockMoveLine
75    pub fn builder() -> StockMoveLineBuilder {
76        <StockMoveLineBuilder as Default>::default()
77    }
78
79    /// Create a new StockMoveLine with required fields
80    pub fn new(quantity: Decimal, picked: bool, state: MoveState, date: DateTime<Utc>, move_id: Uuid, location_id: Uuid, location_dest_id: Uuid, item_id: Uuid) -> Self {
81        Self {
82            id: Uuid::new_v4(),
83            quantity,
84            quantity_product_uom_id: None,
85            picked,
86            lot_id: None,
87            package_id: None,
88            result_package_id: None,
89            owner_id: None,
90            state,
91            date,
92            move_id,
93            picking_id: None,
94            location_id,
95            location_dest_id,
96            item_id,
97            metadata: AuditMetadata::default(),
98        }
99    }
100
101    /// Get the entity's unique identifier
102    pub fn id(&self) -> &Uuid {
103        &self.id
104    }
105
106    /// Get a strongly-typed ID for this entity
107    pub fn typed_id(&self) -> StockMoveLineId {
108        StockMoveLineId(self.id)
109    }
110
111    /// Get when this entity was created
112    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
113        self.metadata.created_at.as_ref()
114    }
115
116    /// Get when this entity was last updated
117    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
118        self.metadata.updated_at.as_ref()
119    }
120
121    /// Check if this entity is soft deleted
122    pub fn is_deleted(&self) -> bool {
123        self.metadata.deleted_at.is_some()
124    }
125
126    /// Check if this entity is active (not deleted)
127    pub fn is_active(&self) -> bool {
128        self.metadata.deleted_at.is_none()
129    }
130
131    /// Get when this entity was deleted
132    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
133        self.metadata.deleted_at.as_ref()
134    }
135
136    /// Get who created this entity
137    pub fn created_by(&self) -> Option<&Uuid> {
138        self.metadata.created_by.as_ref()
139    }
140
141    /// Get who last updated this entity
142    pub fn updated_by(&self) -> Option<&Uuid> {
143        self.metadata.updated_by.as_ref()
144    }
145
146    /// Get who deleted this entity
147    pub fn deleted_by(&self) -> Option<&Uuid> {
148        self.metadata.deleted_by.as_ref()
149    }
150
151
152    // ==========================================================
153    // Fluent Setters (with_* for optional fields)
154    // ==========================================================
155
156    /// Set the quantity_product_uom_id field (chainable)
157    pub fn with_quantity_product_uom_id(mut self, value: Uuid) -> Self {
158        self.quantity_product_uom_id = Some(value);
159        self
160    }
161
162    /// Set the lot_id field (chainable)
163    pub fn with_lot_id(mut self, value: Uuid) -> Self {
164        self.lot_id = Some(value);
165        self
166    }
167
168    /// Set the package_id field (chainable)
169    pub fn with_package_id(mut self, value: Uuid) -> Self {
170        self.package_id = Some(value);
171        self
172    }
173
174    /// Set the result_package_id field (chainable)
175    pub fn with_result_package_id(mut self, value: Uuid) -> Self {
176        self.result_package_id = Some(value);
177        self
178    }
179
180    /// Set the owner_id field (chainable)
181    pub fn with_owner_id(mut self, value: Uuid) -> Self {
182        self.owner_id = Some(value);
183        self
184    }
185
186    /// Set the picking_id field (chainable)
187    pub fn with_picking_id(mut self, value: Uuid) -> Self {
188        self.picking_id = Some(value);
189        self
190    }
191
192    // ==========================================================
193    // Partial Update
194    // ==========================================================
195
196    /// Apply partial updates from a map of field name to JSON value
197    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
198        for (key, value) in fields {
199            match key.as_str() {
200                "quantity" => {
201                    if let Ok(v) = serde_json::from_value(value) { self.quantity = v; }
202                }
203                "quantity_product_uom_id" => {
204                    if let Ok(v) = serde_json::from_value(value) { self.quantity_product_uom_id = v; }
205                }
206                "picked" => {
207                    if let Ok(v) = serde_json::from_value(value) { self.picked = v; }
208                }
209                "lot_id" => {
210                    if let Ok(v) = serde_json::from_value(value) { self.lot_id = v; }
211                }
212                "package_id" => {
213                    if let Ok(v) = serde_json::from_value(value) { self.package_id = v; }
214                }
215                "result_package_id" => {
216                    if let Ok(v) = serde_json::from_value(value) { self.result_package_id = v; }
217                }
218                "owner_id" => {
219                    if let Ok(v) = serde_json::from_value(value) { self.owner_id = v; }
220                }
221                "state" => {
222                    if let Ok(v) = serde_json::from_value(value) { self.state = v; }
223                }
224                "date" => {
225                    if let Ok(v) = serde_json::from_value(value) { self.date = v; }
226                }
227                "move_id" => {
228                    if let Ok(v) = serde_json::from_value(value) { self.move_id = v; }
229                }
230                "picking_id" => {
231                    if let Ok(v) = serde_json::from_value(value) { self.picking_id = v; }
232                }
233                "location_id" => {
234                    if let Ok(v) = serde_json::from_value(value) { self.location_id = v; }
235                }
236                "location_dest_id" => {
237                    if let Ok(v) = serde_json::from_value(value) { self.location_dest_id = v; }
238                }
239                "item_id" => {
240                    if let Ok(v) = serde_json::from_value(value) { self.item_id = v; }
241                }
242                _ => {} // ignore unknown fields
243            }
244        }
245    }
246
247}
248
249impl super::Entity for StockMoveLine {
250    type Id = Uuid;
251
252    fn entity_id(&self) -> &Self::Id {
253        &self.id
254    }
255
256    fn entity_type() -> &'static str {
257        "StockMoveLine"
258    }
259}
260
261impl backbone_core::PersistentEntity for StockMoveLine {
262    fn write_protected_fields() -> &'static [&'static str] {
263        &["state"]
264    }
265    fn entity_id(&self) -> String {
266        self.id.to_string()
267    }
268    fn set_entity_id(&mut self, id: String) {
269        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
270            self.id = uuid;
271        }
272    }
273    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
274        self.metadata.created_at
275    }
276    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
277        self.metadata.created_at = Some(ts);
278    }
279    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
280        self.metadata.updated_at
281    }
282    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
283        self.metadata.updated_at = Some(ts);
284    }
285    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
286        self.metadata.deleted_at
287    }
288    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
289        self.metadata.deleted_at = ts;
290    }
291}
292
293impl backbone_orm::EntityRepoMeta for StockMoveLine {
294    fn column_types() -> std::collections::HashMap<String, String> {
295        let mut m = std::collections::HashMap::new();
296        m.insert("id".to_string(), "uuid".to_string());
297        m.insert("quantity_product_uom_id".to_string(), "uuid".to_string());
298        m.insert("lot_id".to_string(), "uuid".to_string());
299        m.insert("package_id".to_string(), "uuid".to_string());
300        m.insert("result_package_id".to_string(), "uuid".to_string());
301        m.insert("owner_id".to_string(), "uuid".to_string());
302        m.insert("move_id".to_string(), "uuid".to_string());
303        m.insert("picking_id".to_string(), "uuid".to_string());
304        m.insert("location_id".to_string(), "uuid".to_string());
305        m.insert("location_dest_id".to_string(), "uuid".to_string());
306        m.insert("item_id".to_string(), "uuid".to_string());
307        m.insert("state".to_string(), "move_state".to_string());
308        m.insert("date".to_string(), "timestamptz".to_string());
309        m
310    }
311    fn search_fields() -> &'static [&'static str] {
312        &[]
313    }
314    fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
315        &[("move", "stock_moves", "moveId"), ("picking", "transfers", "pickingId"), ("lot", "lots", "lotId"), ("package", "packages", "packageId"), ("resultPackage", "packages", "resultPackageId"), ("location", "locations", "locationId"), ("locationDest", "locations", "locationDestId")]
316    }
317}
318
319/// Builder for StockMoveLine entity
320///
321/// Provides a fluent API for constructing StockMoveLine instances.
322/// System fields (id, metadata, timestamps) are auto-initialized.
323#[derive(Debug, Clone, Default)]
324pub struct StockMoveLineBuilder {
325    quantity: Option<Decimal>,
326    quantity_product_uom_id: Option<Uuid>,
327    picked: Option<bool>,
328    lot_id: Option<Uuid>,
329    package_id: Option<Uuid>,
330    result_package_id: Option<Uuid>,
331    owner_id: Option<Uuid>,
332    state: Option<MoveState>,
333    date: Option<DateTime<Utc>>,
334    move_id: Option<Uuid>,
335    picking_id: Option<Uuid>,
336    location_id: Option<Uuid>,
337    location_dest_id: Option<Uuid>,
338    item_id: Option<Uuid>,
339}
340
341impl StockMoveLineBuilder {
342    /// Set the quantity field (default: `Decimal::from(0)`)
343    pub fn quantity(mut self, value: Decimal) -> Self {
344        self.quantity = Some(value);
345        self
346    }
347
348    /// Set the quantity_product_uom_id field (optional)
349    pub fn quantity_product_uom_id(mut self, value: Uuid) -> Self {
350        self.quantity_product_uom_id = Some(value);
351        self
352    }
353
354    /// Set the picked field (default: `false`)
355    pub fn picked(mut self, value: bool) -> Self {
356        self.picked = Some(value);
357        self
358    }
359
360    /// Set the lot_id field (optional)
361    pub fn lot_id(mut self, value: Uuid) -> Self {
362        self.lot_id = Some(value);
363        self
364    }
365
366    /// Set the package_id field (optional)
367    pub fn package_id(mut self, value: Uuid) -> Self {
368        self.package_id = Some(value);
369        self
370    }
371
372    /// Set the result_package_id field (optional)
373    pub fn result_package_id(mut self, value: Uuid) -> Self {
374        self.result_package_id = Some(value);
375        self
376    }
377
378    /// Set the owner_id field (optional)
379    pub fn owner_id(mut self, value: Uuid) -> Self {
380        self.owner_id = Some(value);
381        self
382    }
383
384    /// Set the state field (default: `MoveState::default()`)
385    pub fn state(mut self, value: MoveState) -> Self {
386        self.state = Some(value);
387        self
388    }
389
390    /// Set the date field (default: `Utc::now()`)
391    pub fn date(mut self, value: DateTime<Utc>) -> Self {
392        self.date = Some(value);
393        self
394    }
395
396    /// Set the move_id field (required)
397    pub fn move_id(mut self, value: Uuid) -> Self {
398        self.move_id = Some(value);
399        self
400    }
401
402    /// Set the picking_id field (optional)
403    pub fn picking_id(mut self, value: Uuid) -> Self {
404        self.picking_id = Some(value);
405        self
406    }
407
408    /// Set the location_id field (required)
409    pub fn location_id(mut self, value: Uuid) -> Self {
410        self.location_id = Some(value);
411        self
412    }
413
414    /// Set the location_dest_id field (required)
415    pub fn location_dest_id(mut self, value: Uuid) -> Self {
416        self.location_dest_id = Some(value);
417        self
418    }
419
420    /// Set the item_id field (required)
421    pub fn item_id(mut self, value: Uuid) -> Self {
422        self.item_id = Some(value);
423        self
424    }
425
426    /// Build the StockMoveLine entity
427    ///
428    /// Returns Err if any required field without a default is missing.
429    pub fn build(self) -> Result<StockMoveLine, String> {
430        let move_id = self.move_id.ok_or_else(|| "move_id is required".to_string())?;
431        let location_id = self.location_id.ok_or_else(|| "location_id is required".to_string())?;
432        let location_dest_id = self.location_dest_id.ok_or_else(|| "location_dest_id is required".to_string())?;
433        let item_id = self.item_id.ok_or_else(|| "item_id is required".to_string())?;
434
435        Ok(StockMoveLine {
436            id: Uuid::new_v4(),
437            quantity: self.quantity.unwrap_or(Decimal::from(0)),
438            quantity_product_uom_id: self.quantity_product_uom_id,
439            picked: self.picked.unwrap_or(false),
440            lot_id: self.lot_id,
441            package_id: self.package_id,
442            result_package_id: self.result_package_id,
443            owner_id: self.owner_id,
444            state: self.state.unwrap_or_default(),
445            date: self.date.unwrap_or(Utc::now()),
446            move_id,
447            picking_id: self.picking_id,
448            location_id,
449            location_dest_id,
450            item_id,
451            metadata: AuditMetadata::default(),
452        })
453    }
454}