backbone_inventory/presentation/dto/
stock_entry_dto.rs1use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use chrono::{DateTime, Utc, NaiveDate};
11
12#[cfg(feature = "openapi")]
13#[cfg(feature = "openapi")]
14use utoipa::ToSchema;
15
16use crate::domain::entity::StockEntry;
17use crate::domain::entity::AuditMetadata;
18use crate::domain::entity::DocStatus;
19use crate::domain::entity::GlPostingState;
20use crate::domain::entity::StockEntryType;
21
22#[derive(Debug, Clone, Deserialize)]
31#[cfg_attr(feature = "openapi", derive(ToSchema))]
32#[serde(rename_all = "camelCase")]
33pub struct CreateStockEntryDto {
34 #[cfg_attr(feature = "openapi", schema(example = "example"))]
35 #[serde(alias = "entry_number")]
36 pub entry_number: String,
37 #[serde(default = "default_create_stock_entry_stock_entry_type", alias = "stock_entry_type")]
38 pub stock_entry_type: StockEntryType,
39 #[serde(default, skip_serializing_if = "Option::is_none", alias = "from_warehouse_id")]
40 pub from_warehouse_id: Option<Uuid>,
41 #[serde(default, skip_serializing_if = "Option::is_none", alias = "to_warehouse_id")]
42 pub to_warehouse_id: Option<Uuid>,
43 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
44 #[serde(alias = "posting_date")]
45 pub posting_date: NaiveDate,
46 #[serde(default, skip_serializing_if = "Option::is_none", alias = "transfer_id")]
47 pub transfer_id: Option<Uuid>,
48 #[serde(default = "default_create_stock_entry_posting_state", alias = "posting_state")]
49 pub posting_state: GlPostingState,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub notes: Option<String>,
52}
53
54fn default_create_stock_entry_stock_entry_type() -> StockEntryType {
55 StockEntryType::Transfer
56}
57
58fn default_create_stock_entry_posting_state() -> GlPostingState {
59 GlPostingState::NotApplicable
60}
61
62#[derive(Debug, Clone, Deserialize)]
71#[cfg_attr(feature = "openapi", derive(ToSchema))]
72#[serde(rename_all = "camelCase")]
73pub struct UpdateStockEntryDto {
74 #[cfg_attr(feature = "openapi", schema(example = "example"))]
75 #[serde(alias = "entry_number")]
76 pub entry_number: String,
77 #[serde(alias = "stock_entry_type")]
78 pub stock_entry_type: StockEntryType,
79 #[serde(default, skip_serializing_if = "Option::is_none", alias = "from_warehouse_id")]
80 pub from_warehouse_id: Option<Uuid>,
81 #[serde(default, skip_serializing_if = "Option::is_none", alias = "to_warehouse_id")]
82 pub to_warehouse_id: Option<Uuid>,
83 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
84 #[serde(alias = "posting_date")]
85 pub posting_date: NaiveDate,
86 #[serde(default, skip_serializing_if = "Option::is_none", alias = "transfer_id")]
87 pub transfer_id: Option<Uuid>,
88 pub status: DocStatus,
89 #[serde(alias = "posting_state")]
90 pub posting_state: GlPostingState,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub notes: Option<String>,
93}
94
95#[derive(Debug, Clone, Default, Deserialize)]
104#[cfg_attr(feature = "openapi", derive(ToSchema))]
105#[serde(rename_all = "camelCase")]
106pub struct PatchStockEntryDto {
107 #[cfg_attr(feature = "openapi", schema(example = "example"))]
108 #[serde(skip_serializing_if = "Option::is_none", alias = "entry_number")]
109 pub entry_number: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none", alias = "stock_entry_type")]
111 pub stock_entry_type: Option<StockEntryType>,
112 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "from_warehouse_id")]
113 pub from_warehouse_id: Option<Option<Uuid>>,
114 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "to_warehouse_id")]
115 pub to_warehouse_id: Option<Option<Uuid>>,
116 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
117 #[serde(skip_serializing_if = "Option::is_none", alias = "posting_date")]
118 pub posting_date: Option<NaiveDate>,
119 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "transfer_id")]
120 pub transfer_id: Option<Option<Uuid>>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub status: Option<DocStatus>,
123 #[serde(skip_serializing_if = "Option::is_none", alias = "posting_state")]
124 pub posting_state: Option<GlPostingState>,
125 #[serde(default, deserialize_with = "crate::presentation::dto::double_option")]
126 pub notes: Option<Option<String>>,
127}
128
129impl PatchStockEntryDto {
130 pub fn has_changes(&self) -> bool {
132 self.entry_number.is_some() || self.stock_entry_type.is_some() || self.from_warehouse_id.is_some() || self.to_warehouse_id.is_some() || self.posting_date.is_some() || self.transfer_id.is_some() || self.status.is_some() || self.posting_state.is_some() || self.notes.is_some()
133 }
134}
135
136#[derive(Debug, Clone, Serialize)]
145#[cfg_attr(feature = "openapi", derive(ToSchema))]
146#[serde(rename_all = "camelCase")]
147pub struct StockEntryResponseDto {
148 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
149 pub id: Uuid,
150 #[cfg_attr(feature = "openapi", schema(example = "example"))]
151 pub entry_number: String,
152 pub stock_entry_type: StockEntryType,
153 pub from_warehouse_id: Option<Uuid>,
154 pub to_warehouse_id: Option<Uuid>,
155 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
156 pub posting_date: NaiveDate,
157 pub transfer_id: Option<Uuid>,
158 pub status: DocStatus,
159 pub posting_state: GlPostingState,
160 pub notes: Option<String>,
161 pub metadata: AuditMetadata,
162}
163
164#[derive(Debug, Clone, Serialize)]
173#[cfg_attr(feature = "openapi", derive(ToSchema))]
174#[serde(rename_all = "camelCase")]
175pub struct StockEntryListResponseDto {
176 pub items: Vec<StockEntryResponseDto>,
178 pub total: u64,
180 pub page: u32,
182 pub per_page: u32,
184 pub total_pages: u32,
186 pub has_next: bool,
188 pub has_prev: bool,
190}
191
192impl StockEntryListResponseDto {
193 pub fn new(items: Vec<StockEntryResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
195 let total_pages = if per_page > 0 {
196 ((total as f64) / (per_page as f64)).ceil() as u32
197 } else {
198 0
199 };
200 Self {
201 items,
202 total,
203 page,
204 per_page,
205 total_pages,
206 has_next: page < total_pages,
207 has_prev: page > 1,
208 }
209 }
210}
211
212#[derive(Debug, Clone, Serialize)]
214#[cfg_attr(feature = "openapi", derive(ToSchema))]
215#[serde(rename_all = "camelCase")]
216pub struct StockEntrySummaryDto {
217 pub id: Uuid,
218 pub entry_number: String,
219 pub stock_entry_type: StockEntryType,
220 pub from_warehouse_id: Option<Uuid>,
221 pub created_at: Option<DateTime<Utc>>,
222}
223
224impl From<StockEntry> for StockEntryResponseDto {
229 fn from(entity: StockEntry) -> Self {
230 Self {
231 id: entity.id,
232 entry_number: entity.entry_number,
233 stock_entry_type: entity.stock_entry_type,
234 from_warehouse_id: entity.from_warehouse_id,
235 to_warehouse_id: entity.to_warehouse_id,
236 posting_date: entity.posting_date,
237 transfer_id: entity.transfer_id,
238 status: entity.status,
239 posting_state: entity.posting_state,
240 notes: entity.notes,
241 metadata: entity.metadata,
242 }
243 }
244}
245
246impl From<StockEntry> for StockEntrySummaryDto {
247 fn from(entity: StockEntry) -> Self {
248 let created_at = backbone_core::PersistentEntity::created_at(&entity);
249 Self {
250 id: entity.id,
251 entry_number: entity.entry_number,
252 stock_entry_type: entity.stock_entry_type,
253 from_warehouse_id: entity.from_warehouse_id,
254 created_at,
255 }
256 }
257}
258
259impl From<CreateStockEntryDto> for StockEntry {
260 fn from(dto: CreateStockEntryDto) -> Self {
261 Self {
262 id: Uuid::new_v4(),
263 entry_number: dto.entry_number,
264 stock_entry_type: dto.stock_entry_type,
265 from_warehouse_id: dto.from_warehouse_id,
266 to_warehouse_id: dto.to_warehouse_id,
267 posting_date: dto.posting_date,
268 transfer_id: dto.transfer_id,
269 status: DocStatus::Draft,
270 posting_state: dto.posting_state,
271 notes: dto.notes,
272 metadata: AuditMetadata::default(),
273 }
274 }
275}
276
277impl From<&StockEntry> for StockEntryResponseDto {
278 fn from(entity: &StockEntry) -> Self {
279 Self {
280 id: entity.id.clone(),
281 entry_number: entity.entry_number.clone(),
282 stock_entry_type: entity.stock_entry_type.clone(),
283 from_warehouse_id: entity.from_warehouse_id.clone(),
284 to_warehouse_id: entity.to_warehouse_id.clone(),
285 posting_date: entity.posting_date.clone(),
286 transfer_id: entity.transfer_id.clone(),
287 status: entity.status.clone(),
288 posting_state: entity.posting_state.clone(),
289 notes: entity.notes.clone(),
290 metadata: entity.metadata.clone(),
291 }
292 }
293}
294
295impl backbone_core::FromCreateDto<CreateStockEntryDto> for StockEntry {
296 fn from_create_dto(dto: CreateStockEntryDto) -> backbone_core::ServiceResult<Self> {
297 Ok(StockEntry::from(dto))
298 }
299}
300
301impl backbone_core::ApplyUpdateDto<UpdateStockEntryDto> for StockEntry {
302 fn apply_update(mut self, dto: UpdateStockEntryDto) -> backbone_core::ServiceResult<Self> {
303 self.entry_number = dto.entry_number;
304 self.stock_entry_type = dto.stock_entry_type;
305 self.from_warehouse_id = dto.from_warehouse_id;
306 self.to_warehouse_id = dto.to_warehouse_id;
307 self.posting_date = dto.posting_date;
308 self.transfer_id = dto.transfer_id;
309 self.status = dto.status;
310 self.posting_state = dto.posting_state;
311 self.notes = dto.notes;
312 Ok(self)
313 }
314}
315