1use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use chrono::{DateTime, Utc, NaiveDate};
11use rust_decimal::Decimal;
12
13#[cfg(feature = "openapi")]
14#[cfg(feature = "openapi")]
15use utoipa::ToSchema;
16
17use crate::domain::entity::DeliveryNote;
18use crate::domain::entity::AuditMetadata;
19use crate::domain::entity::DocStatus;
20use crate::domain::entity::GlPostingState;
21
22#[derive(Debug, Clone, Deserialize)]
31#[cfg_attr(feature = "openapi", derive(ToSchema))]
32#[serde(rename_all = "camelCase")]
33pub struct CreateDeliveryNoteDto {
34 #[cfg_attr(feature = "openapi", schema(example = "example"))]
35 #[serde(alias = "delivery_number")]
36 pub delivery_number: String,
37 #[serde(default, skip_serializing_if = "Option::is_none", alias = "branch_id")]
38 pub branch_id: Option<Uuid>,
39 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
40 #[serde(alias = "customer_id")]
41 pub customer_id: Uuid,
42 #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_so_id")]
43 pub source_so_id: Option<Uuid>,
44 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
45 #[serde(alias = "warehouse_id")]
46 pub warehouse_id: Uuid,
47 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
48 #[serde(alias = "posting_date")]
49 pub posting_date: NaiveDate,
50 #[serde(alias = "total_cogs")]
51 pub total_cogs: Decimal,
52 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
53 #[serde(alias = "cogs_account_id")]
54 pub cogs_account_id: Uuid,
55 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
56 #[serde(alias = "inventory_account_id")]
57 pub inventory_account_id: Uuid,
58 #[serde(default, skip_serializing_if = "Option::is_none", alias = "transfer_id")]
59 pub transfer_id: Option<Uuid>,
60 #[serde(alias = "posting_state")]
61 pub posting_state: GlPostingState,
62 #[serde(default, skip_serializing_if = "Option::is_none", alias = "journal_id")]
63 pub journal_id: Option<Uuid>,
64 #[serde(default, skip_serializing_if = "Option::is_none", alias = "accounting_post_id")]
65 pub accounting_post_id: Option<Uuid>,
66 #[serde(default, skip_serializing_if = "Option::is_none", alias = "posted_at")]
67 pub posted_at: Option<DateTime<Utc>>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub notes: Option<String>,
70}
71
72#[derive(Debug, Clone, Deserialize)]
81#[cfg_attr(feature = "openapi", derive(ToSchema))]
82#[serde(rename_all = "camelCase")]
83pub struct UpdateDeliveryNoteDto {
84 #[cfg_attr(feature = "openapi", schema(example = "example"))]
85 #[serde(alias = "delivery_number")]
86 pub delivery_number: String,
87 #[serde(default, skip_serializing_if = "Option::is_none", alias = "branch_id")]
88 pub branch_id: Option<Uuid>,
89 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
90 #[serde(alias = "customer_id")]
91 pub customer_id: Uuid,
92 #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_so_id")]
93 pub source_so_id: Option<Uuid>,
94 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
95 #[serde(alias = "warehouse_id")]
96 pub warehouse_id: Uuid,
97 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
98 #[serde(alias = "posting_date")]
99 pub posting_date: NaiveDate,
100 #[serde(alias = "total_cogs")]
101 pub total_cogs: Decimal,
102 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
103 #[serde(alias = "cogs_account_id")]
104 pub cogs_account_id: Uuid,
105 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
106 #[serde(alias = "inventory_account_id")]
107 pub inventory_account_id: Uuid,
108 #[serde(default, skip_serializing_if = "Option::is_none", alias = "transfer_id")]
109 pub transfer_id: Option<Uuid>,
110 pub status: DocStatus,
111 #[serde(alias = "posting_state")]
112 pub posting_state: GlPostingState,
113 #[serde(default, skip_serializing_if = "Option::is_none", alias = "journal_id")]
114 pub journal_id: Option<Uuid>,
115 #[serde(default, skip_serializing_if = "Option::is_none", alias = "accounting_post_id")]
116 pub accounting_post_id: Option<Uuid>,
117 #[serde(default, skip_serializing_if = "Option::is_none", alias = "posted_at")]
118 pub posted_at: Option<DateTime<Utc>>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub notes: Option<String>,
121}
122
123#[derive(Debug, Clone, Default, Deserialize)]
132#[cfg_attr(feature = "openapi", derive(ToSchema))]
133#[serde(rename_all = "camelCase")]
134pub struct PatchDeliveryNoteDto {
135 #[cfg_attr(feature = "openapi", schema(example = "example"))]
136 #[serde(skip_serializing_if = "Option::is_none", alias = "delivery_number")]
137 pub delivery_number: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none", alias = "branch_id")]
139 pub branch_id: Option<Uuid>,
140 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
141 #[serde(skip_serializing_if = "Option::is_none", alias = "customer_id")]
142 pub customer_id: Option<Uuid>,
143 #[serde(skip_serializing_if = "Option::is_none", alias = "source_so_id")]
144 pub source_so_id: Option<Uuid>,
145 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
146 #[serde(skip_serializing_if = "Option::is_none", alias = "warehouse_id")]
147 pub warehouse_id: Option<Uuid>,
148 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
149 #[serde(skip_serializing_if = "Option::is_none", alias = "posting_date")]
150 pub posting_date: Option<NaiveDate>,
151 #[serde(skip_serializing_if = "Option::is_none", alias = "total_cogs")]
152 pub total_cogs: Option<Decimal>,
153 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
154 #[serde(skip_serializing_if = "Option::is_none", alias = "cogs_account_id")]
155 pub cogs_account_id: Option<Uuid>,
156 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
157 #[serde(skip_serializing_if = "Option::is_none", alias = "inventory_account_id")]
158 pub inventory_account_id: Option<Uuid>,
159 #[serde(skip_serializing_if = "Option::is_none", alias = "transfer_id")]
160 pub transfer_id: Option<Uuid>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub status: Option<DocStatus>,
163 #[serde(skip_serializing_if = "Option::is_none", alias = "posting_state")]
164 pub posting_state: Option<GlPostingState>,
165 #[serde(skip_serializing_if = "Option::is_none", alias = "journal_id")]
166 pub journal_id: Option<Uuid>,
167 #[serde(skip_serializing_if = "Option::is_none", alias = "accounting_post_id")]
168 pub accounting_post_id: Option<Uuid>,
169 #[serde(skip_serializing_if = "Option::is_none", alias = "posted_at")]
170 pub posted_at: Option<DateTime<Utc>>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub notes: Option<String>,
173}
174
175impl PatchDeliveryNoteDto {
176 pub fn has_changes(&self) -> bool {
178 self.delivery_number.is_some() || self.branch_id.is_some() || self.customer_id.is_some() || self.source_so_id.is_some() || self.warehouse_id.is_some() || self.posting_date.is_some() || self.total_cogs.is_some() || self.cogs_account_id.is_some() || self.inventory_account_id.is_some() || self.transfer_id.is_some() || self.status.is_some() || self.posting_state.is_some() || self.journal_id.is_some() || self.accounting_post_id.is_some() || self.posted_at.is_some() || self.notes.is_some()
179 }
180}
181
182#[derive(Debug, Clone, Serialize)]
191#[cfg_attr(feature = "openapi", derive(ToSchema))]
192#[serde(rename_all = "camelCase")]
193pub struct DeliveryNoteResponseDto {
194 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
195 pub id: Uuid,
196 #[cfg_attr(feature = "openapi", schema(example = "example"))]
197 pub delivery_number: String,
198 pub branch_id: Option<Uuid>,
199 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
200 pub customer_id: Uuid,
201 pub source_so_id: Option<Uuid>,
202 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
203 pub warehouse_id: Uuid,
204 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
205 pub posting_date: NaiveDate,
206 pub total_cogs: Decimal,
207 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
208 pub cogs_account_id: Uuid,
209 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
210 pub inventory_account_id: Uuid,
211 pub transfer_id: Option<Uuid>,
212 pub status: DocStatus,
213 pub posting_state: GlPostingState,
214 pub journal_id: Option<Uuid>,
215 pub accounting_post_id: Option<Uuid>,
216 pub posted_at: Option<DateTime<Utc>>,
217 pub notes: Option<String>,
218 pub metadata: AuditMetadata,
219}
220
221#[derive(Debug, Clone, Serialize)]
230#[cfg_attr(feature = "openapi", derive(ToSchema))]
231#[serde(rename_all = "camelCase")]
232pub struct DeliveryNoteListResponseDto {
233 pub items: Vec<DeliveryNoteResponseDto>,
235 pub total: u64,
237 pub page: u32,
239 pub per_page: u32,
241 pub total_pages: u32,
243 pub has_next: bool,
245 pub has_prev: bool,
247}
248
249impl DeliveryNoteListResponseDto {
250 pub fn new(items: Vec<DeliveryNoteResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
252 let total_pages = if per_page > 0 {
253 ((total as f64) / (per_page as f64)).ceil() as u32
254 } else {
255 0
256 };
257 Self {
258 items,
259 total,
260 page,
261 per_page,
262 total_pages,
263 has_next: page < total_pages,
264 has_prev: page > 1,
265 }
266 }
267}
268
269#[derive(Debug, Clone, Serialize)]
271#[cfg_attr(feature = "openapi", derive(ToSchema))]
272#[serde(rename_all = "camelCase")]
273pub struct DeliveryNoteSummaryDto {
274 pub id: Uuid,
275 pub delivery_number: String,
276 pub branch_id: Option<Uuid>,
277 pub customer_id: Uuid,
278 pub created_at: Option<DateTime<Utc>>,
279}
280
281impl From<DeliveryNote> for DeliveryNoteResponseDto {
286 fn from(entity: DeliveryNote) -> Self {
287 Self {
288 id: entity.id,
289 delivery_number: entity.delivery_number,
290 branch_id: entity.branch_id,
291 customer_id: entity.customer_id,
292 source_so_id: entity.source_so_id,
293 warehouse_id: entity.warehouse_id,
294 posting_date: entity.posting_date,
295 total_cogs: entity.total_cogs,
296 cogs_account_id: entity.cogs_account_id,
297 inventory_account_id: entity.inventory_account_id,
298 transfer_id: entity.transfer_id,
299 status: entity.status,
300 posting_state: entity.posting_state,
301 journal_id: entity.journal_id,
302 accounting_post_id: entity.accounting_post_id,
303 posted_at: entity.posted_at,
304 notes: entity.notes,
305 metadata: entity.metadata,
306 }
307 }
308}
309
310impl From<DeliveryNote> for DeliveryNoteSummaryDto {
311 fn from(entity: DeliveryNote) -> Self {
312 let created_at = backbone_core::PersistentEntity::created_at(&entity);
313 Self {
314 id: entity.id,
315 delivery_number: entity.delivery_number,
316 branch_id: entity.branch_id,
317 customer_id: entity.customer_id,
318 created_at,
319 }
320 }
321}
322
323impl From<CreateDeliveryNoteDto> for DeliveryNote {
324 fn from(dto: CreateDeliveryNoteDto) -> Self {
325 Self {
326 id: Uuid::new_v4(),
327 delivery_number: dto.delivery_number,
328 branch_id: dto.branch_id,
329 customer_id: dto.customer_id,
330 source_so_id: dto.source_so_id,
331 warehouse_id: dto.warehouse_id,
332 posting_date: dto.posting_date,
333 total_cogs: dto.total_cogs,
334 cogs_account_id: dto.cogs_account_id,
335 inventory_account_id: dto.inventory_account_id,
336 transfer_id: dto.transfer_id,
337 status: DocStatus::Draft,
338 posting_state: dto.posting_state,
339 journal_id: dto.journal_id,
340 accounting_post_id: dto.accounting_post_id,
341 posted_at: dto.posted_at,
342 notes: dto.notes,
343 metadata: AuditMetadata::default(),
344 }
345 }
346}
347
348impl From<&DeliveryNote> for DeliveryNoteResponseDto {
349 fn from(entity: &DeliveryNote) -> Self {
350 Self {
351 id: entity.id.clone(),
352 delivery_number: entity.delivery_number.clone(),
353 branch_id: entity.branch_id.clone(),
354 customer_id: entity.customer_id.clone(),
355 source_so_id: entity.source_so_id.clone(),
356 warehouse_id: entity.warehouse_id.clone(),
357 posting_date: entity.posting_date.clone(),
358 total_cogs: entity.total_cogs.clone(),
359 cogs_account_id: entity.cogs_account_id.clone(),
360 inventory_account_id: entity.inventory_account_id.clone(),
361 transfer_id: entity.transfer_id.clone(),
362 status: entity.status.clone(),
363 posting_state: entity.posting_state.clone(),
364 journal_id: entity.journal_id.clone(),
365 accounting_post_id: entity.accounting_post_id.clone(),
366 posted_at: entity.posted_at.clone(),
367 notes: entity.notes.clone(),
368 metadata: entity.metadata.clone(),
369 }
370 }
371}
372
373impl backbone_core::FromCreateDto<CreateDeliveryNoteDto> for DeliveryNote {
374 fn from_create_dto(dto: CreateDeliveryNoteDto) -> backbone_core::ServiceResult<Self> {
375 Ok(DeliveryNote::from(dto))
376 }
377}
378
379impl backbone_core::ApplyUpdateDto<UpdateDeliveryNoteDto> for DeliveryNote {
380 fn apply_update(mut self, dto: UpdateDeliveryNoteDto) -> backbone_core::ServiceResult<Self> {
381 self.delivery_number = dto.delivery_number;
382 self.branch_id = dto.branch_id;
383 self.customer_id = dto.customer_id;
384 self.source_so_id = dto.source_so_id;
385 self.warehouse_id = dto.warehouse_id;
386 self.posting_date = dto.posting_date;
387 self.total_cogs = dto.total_cogs;
388 self.cogs_account_id = dto.cogs_account_id;
389 self.inventory_account_id = dto.inventory_account_id;
390 self.transfer_id = dto.transfer_id;
391 self.status = dto.status;
392 self.posting_state = dto.posting_state;
393 self.journal_id = dto.journal_id;
394 self.accounting_post_id = dto.accounting_post_id;
395 self.posted_at = dto.posted_at;
396 self.notes = dto.notes;
397 Ok(self)
398 }
399}
400
401