Skip to main content

backbone_inventory/domain/entity/
stock_move.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::GlPostingState;
9use super::Priority;
10use super::ProcureMethod;
11use super::AuditMetadata;
12
13/// Strongly-typed ID for StockMove
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct StockMoveId(pub Uuid);
17
18impl StockMoveId {
19    pub fn new(id: Uuid) -> Self { Self(id) }
20    pub fn generate() -> Self { Self(Uuid::new_v4()) }
21    pub fn into_inner(self) -> Uuid { self.0 }
22}
23
24impl std::fmt::Display for StockMoveId {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "{}", self.0)
27    }
28}
29
30impl std::str::FromStr for StockMoveId {
31    type Err = uuid::Error;
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        Ok(Self(Uuid::parse_str(s)?))
34    }
35}
36
37impl From<Uuid> for StockMoveId {
38    fn from(id: Uuid) -> Self { Self(id) }
39}
40
41impl From<StockMoveId> for Uuid {
42    fn from(id: StockMoveId) -> Self { id.0 }
43}
44
45impl AsRef<Uuid> for StockMoveId {
46    fn as_ref(&self) -> &Uuid { &self.0 }
47}
48
49impl std::ops::Deref for StockMoveId {
50    type Target = Uuid;
51    fn deref(&self) -> &Self::Target { &self.0 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
55pub struct StockMove {
56    pub id: Uuid,
57    pub name: String,
58    pub state: MoveState,
59    pub posting_state: GlPostingState,
60    pub priority: Priority,
61    pub create_date: DateTime<Utc>,
62    pub date: DateTime<Utc>,
63    pub item_id: Uuid,
64    pub demand_qty: Decimal,
65    pub quantity: Decimal,
66    pub price_unit: Decimal,
67    pub forced_value: Option<Decimal>,
68    pub procure_method: ProcureMethod,
69    pub picking_id: Option<Uuid>,
70    pub origin: Option<String>,
71    pub location_id: Uuid,
72    pub location_dest_id: Uuid,
73    pub partner_id: Option<Uuid>,
74    pub rule_id: Option<Uuid>,
75    pub warehouse_id: Option<Uuid>,
76    pub orderpoint_id: Option<Uuid>,
77    pub move_orig_ids: Vec<Uuid>,
78    pub move_dest_ids: Vec<Uuid>,
79    pub is_inventory: bool,
80    pub scrapped: bool,
81    pub propagate_cancel: bool,
82    #[serde(default)]
83    #[sqlx(json)]
84    pub metadata: AuditMetadata,
85}
86
87impl StockMove {
88    /// Create a builder for StockMove
89    pub fn builder() -> StockMoveBuilder {
90        <StockMoveBuilder as Default>::default()
91    }
92
93    /// Create a new StockMove with required fields
94    pub fn new(name: String, state: MoveState, posting_state: GlPostingState, priority: Priority, create_date: DateTime<Utc>, date: DateTime<Utc>, item_id: Uuid, demand_qty: Decimal, quantity: Decimal, price_unit: Decimal, procure_method: ProcureMethod, location_id: Uuid, location_dest_id: Uuid, move_orig_ids: Vec<Uuid>, move_dest_ids: Vec<Uuid>, is_inventory: bool, scrapped: bool, propagate_cancel: bool) -> Self {
95        Self {
96            id: Uuid::new_v4(),
97            name,
98            state,
99            posting_state,
100            priority,
101            create_date,
102            date,
103            item_id,
104            demand_qty,
105            quantity,
106            price_unit,
107            forced_value: None,
108            procure_method,
109            picking_id: None,
110            origin: None,
111            location_id,
112            location_dest_id,
113            partner_id: None,
114            rule_id: None,
115            warehouse_id: None,
116            orderpoint_id: None,
117            move_orig_ids,
118            move_dest_ids,
119            is_inventory,
120            scrapped,
121            propagate_cancel,
122            metadata: AuditMetadata::default(),
123        }
124    }
125
126    /// Get the entity's unique identifier
127    pub fn id(&self) -> &Uuid {
128        &self.id
129    }
130
131    /// Get a strongly-typed ID for this entity
132    pub fn typed_id(&self) -> StockMoveId {
133        StockMoveId(self.id)
134    }
135
136    /// Get when this entity was created
137    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
138        self.metadata.created_at.as_ref()
139    }
140
141    /// Get when this entity was last updated
142    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
143        self.metadata.updated_at.as_ref()
144    }
145
146    /// Check if this entity is soft deleted
147    pub fn is_deleted(&self) -> bool {
148        self.metadata.deleted_at.is_some()
149    }
150
151    /// Check if this entity is active (not deleted)
152    pub fn is_active(&self) -> bool {
153        self.metadata.deleted_at.is_none()
154    }
155
156    /// Get when this entity was deleted
157    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
158        self.metadata.deleted_at.as_ref()
159    }
160
161    /// Get who created this entity
162    pub fn created_by(&self) -> Option<&Uuid> {
163        self.metadata.created_by.as_ref()
164    }
165
166    /// Get who last updated this entity
167    pub fn updated_by(&self) -> Option<&Uuid> {
168        self.metadata.updated_by.as_ref()
169    }
170
171    /// Get who deleted this entity
172    pub fn deleted_by(&self) -> Option<&Uuid> {
173        self.metadata.deleted_by.as_ref()
174    }
175
176
177    // ==========================================================
178    // Fluent Setters (with_* for optional fields)
179    // ==========================================================
180
181    /// Set the forced_value field (chainable)
182    pub fn with_forced_value(mut self, value: Decimal) -> Self {
183        self.forced_value = Some(value);
184        self
185    }
186
187    /// Set the picking_id field (chainable)
188    pub fn with_picking_id(mut self, value: Uuid) -> Self {
189        self.picking_id = Some(value);
190        self
191    }
192
193    /// Set the origin field (chainable)
194    pub fn with_origin(mut self, value: String) -> Self {
195        self.origin = Some(value);
196        self
197    }
198
199    /// Set the partner_id field (chainable)
200    pub fn with_partner_id(mut self, value: Uuid) -> Self {
201        self.partner_id = Some(value);
202        self
203    }
204
205    /// Set the rule_id field (chainable)
206    pub fn with_rule_id(mut self, value: Uuid) -> Self {
207        self.rule_id = Some(value);
208        self
209    }
210
211    /// Set the warehouse_id field (chainable)
212    pub fn with_warehouse_id(mut self, value: Uuid) -> Self {
213        self.warehouse_id = Some(value);
214        self
215    }
216
217    /// Set the orderpoint_id field (chainable)
218    pub fn with_orderpoint_id(mut self, value: Uuid) -> Self {
219        self.orderpoint_id = Some(value);
220        self
221    }
222
223    // ==========================================================
224    // Partial Update
225    // ==========================================================
226
227    /// Apply partial updates from a map of field name to JSON value
228    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
229        for (key, value) in fields {
230            match key.as_str() {
231                "name" => {
232                    if let Ok(v) = serde_json::from_value(value) { self.name = v; }
233                }
234                "state" => {
235                    if let Ok(v) = serde_json::from_value(value) { self.state = v; }
236                }
237                "posting_state" => {
238                    if let Ok(v) = serde_json::from_value(value) { self.posting_state = v; }
239                }
240                "priority" => {
241                    if let Ok(v) = serde_json::from_value(value) { self.priority = v; }
242                }
243                "create_date" => {
244                    if let Ok(v) = serde_json::from_value(value) { self.create_date = v; }
245                }
246                "date" => {
247                    if let Ok(v) = serde_json::from_value(value) { self.date = v; }
248                }
249                "item_id" => {
250                    if let Ok(v) = serde_json::from_value(value) { self.item_id = v; }
251                }
252                "demand_qty" => {
253                    if let Ok(v) = serde_json::from_value(value) { self.demand_qty = v; }
254                }
255                "quantity" => {
256                    if let Ok(v) = serde_json::from_value(value) { self.quantity = v; }
257                }
258                "price_unit" => {
259                    if let Ok(v) = serde_json::from_value(value) { self.price_unit = v; }
260                }
261                "forced_value" => {
262                    if let Ok(v) = serde_json::from_value(value) { self.forced_value = v; }
263                }
264                "procure_method" => {
265                    if let Ok(v) = serde_json::from_value(value) { self.procure_method = v; }
266                }
267                "picking_id" => {
268                    if let Ok(v) = serde_json::from_value(value) { self.picking_id = v; }
269                }
270                "origin" => {
271                    if let Ok(v) = serde_json::from_value(value) { self.origin = v; }
272                }
273                "location_id" => {
274                    if let Ok(v) = serde_json::from_value(value) { self.location_id = v; }
275                }
276                "location_dest_id" => {
277                    if let Ok(v) = serde_json::from_value(value) { self.location_dest_id = v; }
278                }
279                "partner_id" => {
280                    if let Ok(v) = serde_json::from_value(value) { self.partner_id = v; }
281                }
282                "rule_id" => {
283                    if let Ok(v) = serde_json::from_value(value) { self.rule_id = v; }
284                }
285                "warehouse_id" => {
286                    if let Ok(v) = serde_json::from_value(value) { self.warehouse_id = v; }
287                }
288                "orderpoint_id" => {
289                    if let Ok(v) = serde_json::from_value(value) { self.orderpoint_id = v; }
290                }
291                "move_orig_ids" => {
292                    if let Ok(v) = serde_json::from_value(value) { self.move_orig_ids = v; }
293                }
294                "move_dest_ids" => {
295                    if let Ok(v) = serde_json::from_value(value) { self.move_dest_ids = v; }
296                }
297                "is_inventory" => {
298                    if let Ok(v) = serde_json::from_value(value) { self.is_inventory = v; }
299                }
300                "scrapped" => {
301                    if let Ok(v) = serde_json::from_value(value) { self.scrapped = v; }
302                }
303                "propagate_cancel" => {
304                    if let Ok(v) = serde_json::from_value(value) { self.propagate_cancel = v; }
305                }
306                _ => {} // ignore unknown fields
307            }
308        }
309    }
310
311}
312
313impl super::Entity for StockMove {
314    type Id = Uuid;
315
316    fn entity_id(&self) -> &Self::Id {
317        &self.id
318    }
319
320    fn entity_type() -> &'static str {
321        "StockMove"
322    }
323}
324
325impl backbone_core::PersistentEntity for StockMove {
326    fn write_protected_fields() -> &'static [&'static str] {
327        &["state"]
328    }
329    fn entity_id(&self) -> String {
330        self.id.to_string()
331    }
332    fn set_entity_id(&mut self, id: String) {
333        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
334            self.id = uuid;
335        }
336    }
337    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
338        self.metadata.created_at
339    }
340    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
341        self.metadata.created_at = Some(ts);
342    }
343    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
344        self.metadata.updated_at
345    }
346    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
347        self.metadata.updated_at = Some(ts);
348    }
349    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
350        self.metadata.deleted_at
351    }
352    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
353        self.metadata.deleted_at = ts;
354    }
355}
356
357impl backbone_orm::EntityRepoMeta for StockMove {
358    fn column_types() -> std::collections::HashMap<String, String> {
359        let mut m = std::collections::HashMap::new();
360        m.insert("id".to_string(), "uuid".to_string());
361        m.insert("item_id".to_string(), "uuid".to_string());
362        m.insert("picking_id".to_string(), "uuid".to_string());
363        m.insert("location_id".to_string(), "uuid".to_string());
364        m.insert("location_dest_id".to_string(), "uuid".to_string());
365        m.insert("partner_id".to_string(), "uuid".to_string());
366        m.insert("rule_id".to_string(), "uuid".to_string());
367        m.insert("warehouse_id".to_string(), "uuid".to_string());
368        m.insert("orderpoint_id".to_string(), "uuid".to_string());
369        m.insert("state".to_string(), "move_state".to_string());
370        m.insert("posting_state".to_string(), "gl_posting_state".to_string());
371        m.insert("priority".to_string(), "priority".to_string());
372        m.insert("procure_method".to_string(), "procure_method".to_string());
373        m.insert("create_date".to_string(), "timestamptz".to_string());
374        m.insert("date".to_string(), "timestamptz".to_string());
375        m
376    }
377    fn search_fields() -> &'static [&'static str] {
378        &["name"]
379    }
380    fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
381        &[("picking", "transfers", "pickingId"), ("location", "locations", "locationId"), ("locationDest", "locations", "locationDestId"), ("rule", "route_rules", "ruleId"), ("warehouse", "warehouses", "warehouseId"), ("orderpoint", "reordering_rules", "orderpointId")]
382    }
383}
384
385/// Builder for StockMove entity
386///
387/// Provides a fluent API for constructing StockMove instances.
388/// System fields (id, metadata, timestamps) are auto-initialized.
389#[derive(Debug, Clone, Default)]
390pub struct StockMoveBuilder {
391    name: Option<String>,
392    state: Option<MoveState>,
393    posting_state: Option<GlPostingState>,
394    priority: Option<Priority>,
395    create_date: Option<DateTime<Utc>>,
396    date: Option<DateTime<Utc>>,
397    item_id: Option<Uuid>,
398    demand_qty: Option<Decimal>,
399    quantity: Option<Decimal>,
400    price_unit: Option<Decimal>,
401    forced_value: Option<Decimal>,
402    procure_method: Option<ProcureMethod>,
403    picking_id: Option<Uuid>,
404    origin: Option<String>,
405    location_id: Option<Uuid>,
406    location_dest_id: Option<Uuid>,
407    partner_id: Option<Uuid>,
408    rule_id: Option<Uuid>,
409    warehouse_id: Option<Uuid>,
410    orderpoint_id: Option<Uuid>,
411    move_orig_ids: Option<Vec<Uuid>>,
412    move_dest_ids: Option<Vec<Uuid>>,
413    is_inventory: Option<bool>,
414    scrapped: Option<bool>,
415    propagate_cancel: Option<bool>,
416}
417
418impl StockMoveBuilder {
419    /// Set the name field (required)
420    pub fn name(mut self, value: String) -> Self {
421        self.name = Some(value);
422        self
423    }
424
425    /// Set the state field (default: `MoveState::default()`)
426    pub fn state(mut self, value: MoveState) -> Self {
427        self.state = Some(value);
428        self
429    }
430
431    /// Set the posting_state field (default: `GlPostingState::default()`)
432    pub fn posting_state(mut self, value: GlPostingState) -> Self {
433        self.posting_state = Some(value);
434        self
435    }
436
437    /// Set the priority field (default: `Priority::default()`)
438    pub fn priority(mut self, value: Priority) -> Self {
439        self.priority = Some(value);
440        self
441    }
442
443    /// Set the create_date field (default: `Utc::now()`)
444    pub fn create_date(mut self, value: DateTime<Utc>) -> Self {
445        self.create_date = Some(value);
446        self
447    }
448
449    /// Set the date field (default: `Utc::now()`)
450    pub fn date(mut self, value: DateTime<Utc>) -> Self {
451        self.date = Some(value);
452        self
453    }
454
455    /// Set the item_id field (required)
456    pub fn item_id(mut self, value: Uuid) -> Self {
457        self.item_id = Some(value);
458        self
459    }
460
461    /// Set the demand_qty field (default: `Decimal::from(0)`)
462    pub fn demand_qty(mut self, value: Decimal) -> Self {
463        self.demand_qty = Some(value);
464        self
465    }
466
467    /// Set the quantity field (default: `Decimal::from(0)`)
468    pub fn quantity(mut self, value: Decimal) -> Self {
469        self.quantity = Some(value);
470        self
471    }
472
473    /// Set the price_unit field (default: `Decimal::from(0)`)
474    pub fn price_unit(mut self, value: Decimal) -> Self {
475        self.price_unit = Some(value);
476        self
477    }
478
479    /// Set the forced_value field (optional)
480    pub fn forced_value(mut self, value: Decimal) -> Self {
481        self.forced_value = Some(value);
482        self
483    }
484
485    /// Set the procure_method field (default: `ProcureMethod::default()`)
486    pub fn procure_method(mut self, value: ProcureMethod) -> Self {
487        self.procure_method = Some(value);
488        self
489    }
490
491    /// Set the picking_id field (optional)
492    pub fn picking_id(mut self, value: Uuid) -> Self {
493        self.picking_id = Some(value);
494        self
495    }
496
497    /// Set the origin field (optional)
498    pub fn origin(mut self, value: String) -> Self {
499        self.origin = Some(value);
500        self
501    }
502
503    /// Set the location_id field (required)
504    pub fn location_id(mut self, value: Uuid) -> Self {
505        self.location_id = Some(value);
506        self
507    }
508
509    /// Set the location_dest_id field (required)
510    pub fn location_dest_id(mut self, value: Uuid) -> Self {
511        self.location_dest_id = Some(value);
512        self
513    }
514
515    /// Set the partner_id field (optional)
516    pub fn partner_id(mut self, value: Uuid) -> Self {
517        self.partner_id = Some(value);
518        self
519    }
520
521    /// Set the rule_id field (optional)
522    pub fn rule_id(mut self, value: Uuid) -> Self {
523        self.rule_id = Some(value);
524        self
525    }
526
527    /// Set the warehouse_id field (optional)
528    pub fn warehouse_id(mut self, value: Uuid) -> Self {
529        self.warehouse_id = Some(value);
530        self
531    }
532
533    /// Set the orderpoint_id field (optional)
534    pub fn orderpoint_id(mut self, value: Uuid) -> Self {
535        self.orderpoint_id = Some(value);
536        self
537    }
538
539    /// Set the move_orig_ids field (required)
540    pub fn move_orig_ids(mut self, value: Vec<Uuid>) -> Self {
541        self.move_orig_ids = Some(value);
542        self
543    }
544
545    /// Set the move_dest_ids field (required)
546    pub fn move_dest_ids(mut self, value: Vec<Uuid>) -> Self {
547        self.move_dest_ids = Some(value);
548        self
549    }
550
551    /// Set the is_inventory field (default: `false`)
552    pub fn is_inventory(mut self, value: bool) -> Self {
553        self.is_inventory = Some(value);
554        self
555    }
556
557    /// Set the scrapped field (default: `false`)
558    pub fn scrapped(mut self, value: bool) -> Self {
559        self.scrapped = Some(value);
560        self
561    }
562
563    /// Set the propagate_cancel field (default: `true`)
564    pub fn propagate_cancel(mut self, value: bool) -> Self {
565        self.propagate_cancel = Some(value);
566        self
567    }
568
569    /// Build the StockMove entity
570    ///
571    /// Returns Err if any required field without a default is missing.
572    pub fn build(self) -> Result<StockMove, String> {
573        let name = self.name.ok_or_else(|| "name is required".to_string())?;
574        let item_id = self.item_id.ok_or_else(|| "item_id is required".to_string())?;
575        let location_id = self.location_id.ok_or_else(|| "location_id is required".to_string())?;
576        let location_dest_id = self.location_dest_id.ok_or_else(|| "location_dest_id is required".to_string())?;
577        let move_orig_ids = self.move_orig_ids.ok_or_else(|| "move_orig_ids is required".to_string())?;
578        let move_dest_ids = self.move_dest_ids.ok_or_else(|| "move_dest_ids is required".to_string())?;
579
580        Ok(StockMove {
581            id: Uuid::new_v4(),
582            name,
583            state: self.state.unwrap_or_default(),
584            posting_state: self.posting_state.unwrap_or_default(),
585            priority: self.priority.unwrap_or_default(),
586            create_date: self.create_date.unwrap_or(Utc::now()),
587            date: self.date.unwrap_or(Utc::now()),
588            item_id,
589            demand_qty: self.demand_qty.unwrap_or(Decimal::from(0)),
590            quantity: self.quantity.unwrap_or(Decimal::from(0)),
591            price_unit: self.price_unit.unwrap_or(Decimal::from(0)),
592            forced_value: self.forced_value,
593            procure_method: self.procure_method.unwrap_or_default(),
594            picking_id: self.picking_id,
595            origin: self.origin,
596            location_id,
597            location_dest_id,
598            partner_id: self.partner_id,
599            rule_id: self.rule_id,
600            warehouse_id: self.warehouse_id,
601            orderpoint_id: self.orderpoint_id,
602            move_orig_ids,
603            move_dest_ids,
604            is_inventory: self.is_inventory.unwrap_or(false),
605            scrapped: self.scrapped.unwrap_or(false),
606            propagate_cancel: self.propagate_cancel.unwrap_or(true),
607            metadata: AuditMetadata::default(),
608        })
609    }
610}