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(default, 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(default = "default_create_delivery_note_posting_state", 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
72fn default_create_delivery_note_posting_state() -> GlPostingState {
73 GlPostingState::Pending
74}
75
76#[derive(Debug, Clone, Deserialize)]
85#[cfg_attr(feature = "openapi", derive(ToSchema))]
86#[serde(rename_all = "camelCase")]
87pub struct UpdateDeliveryNoteDto {
88 #[cfg_attr(feature = "openapi", schema(example = "example"))]
89 #[serde(alias = "delivery_number")]
90 pub delivery_number: String,
91 #[serde(default, skip_serializing_if = "Option::is_none", alias = "branch_id")]
92 pub branch_id: Option<Uuid>,
93 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
94 #[serde(alias = "customer_id")]
95 pub customer_id: Uuid,
96 #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_so_id")]
97 pub source_so_id: Option<Uuid>,
98 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
99 #[serde(alias = "warehouse_id")]
100 pub warehouse_id: Uuid,
101 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
102 #[serde(alias = "posting_date")]
103 pub posting_date: NaiveDate,
104 #[serde(alias = "total_cogs")]
105 pub total_cogs: Decimal,
106 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
107 #[serde(alias = "cogs_account_id")]
108 pub cogs_account_id: Uuid,
109 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
110 #[serde(alias = "inventory_account_id")]
111 pub inventory_account_id: Uuid,
112 #[serde(default, skip_serializing_if = "Option::is_none", alias = "transfer_id")]
113 pub transfer_id: Option<Uuid>,
114 pub status: DocStatus,
115 #[serde(alias = "posting_state")]
116 pub posting_state: GlPostingState,
117 #[serde(default, skip_serializing_if = "Option::is_none", alias = "journal_id")]
118 pub journal_id: Option<Uuid>,
119 #[serde(default, skip_serializing_if = "Option::is_none", alias = "accounting_post_id")]
120 pub accounting_post_id: Option<Uuid>,
121 #[serde(default, skip_serializing_if = "Option::is_none", alias = "posted_at")]
122 pub posted_at: Option<DateTime<Utc>>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub notes: Option<String>,
125}
126
127#[derive(Debug, Clone, Default, Deserialize)]
136#[cfg_attr(feature = "openapi", derive(ToSchema))]
137#[serde(rename_all = "camelCase")]
138pub struct PatchDeliveryNoteDto {
139 #[cfg_attr(feature = "openapi", schema(example = "example"))]
140 #[serde(skip_serializing_if = "Option::is_none", alias = "delivery_number")]
141 pub delivery_number: Option<String>,
142 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "branch_id")]
143 pub branch_id: Option<Option<Uuid>>,
144 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
145 #[serde(skip_serializing_if = "Option::is_none", alias = "customer_id")]
146 pub customer_id: Option<Uuid>,
147 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "source_so_id")]
148 pub source_so_id: Option<Option<Uuid>>,
149 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
150 #[serde(skip_serializing_if = "Option::is_none", alias = "warehouse_id")]
151 pub warehouse_id: Option<Uuid>,
152 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
153 #[serde(skip_serializing_if = "Option::is_none", alias = "posting_date")]
154 pub posting_date: Option<NaiveDate>,
155 #[serde(skip_serializing_if = "Option::is_none", alias = "total_cogs")]
156 pub total_cogs: Option<Decimal>,
157 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
158 #[serde(skip_serializing_if = "Option::is_none", alias = "cogs_account_id")]
159 pub cogs_account_id: Option<Uuid>,
160 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
161 #[serde(skip_serializing_if = "Option::is_none", alias = "inventory_account_id")]
162 pub inventory_account_id: Option<Uuid>,
163 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "transfer_id")]
164 pub transfer_id: Option<Option<Uuid>>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub status: Option<DocStatus>,
167 #[serde(skip_serializing_if = "Option::is_none", alias = "posting_state")]
168 pub posting_state: Option<GlPostingState>,
169 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "journal_id")]
170 pub journal_id: Option<Option<Uuid>>,
171 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "accounting_post_id")]
172 pub accounting_post_id: Option<Option<Uuid>>,
173 #[serde(default, deserialize_with = "crate::presentation::dto::double_option", alias = "posted_at")]
174 pub posted_at: Option<Option<DateTime<Utc>>>,
175 #[serde(default, deserialize_with = "crate::presentation::dto::double_option")]
176 pub notes: Option<Option<String>>,
177}
178
179impl PatchDeliveryNoteDto {
180 pub fn has_changes(&self) -> bool {
182 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()
183 }
184}
185
186#[derive(Debug, Clone, Serialize)]
195#[cfg_attr(feature = "openapi", derive(ToSchema))]
196#[serde(rename_all = "camelCase")]
197pub struct DeliveryNoteResponseDto {
198 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
199 pub id: Uuid,
200 #[cfg_attr(feature = "openapi", schema(example = "example"))]
201 pub delivery_number: String,
202 pub branch_id: Option<Uuid>,
203 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
204 pub customer_id: Uuid,
205 pub source_so_id: Option<Uuid>,
206 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
207 pub warehouse_id: Uuid,
208 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
209 pub posting_date: NaiveDate,
210 pub total_cogs: Decimal,
211 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
212 pub cogs_account_id: Uuid,
213 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
214 pub inventory_account_id: Uuid,
215 pub transfer_id: Option<Uuid>,
216 pub status: DocStatus,
217 pub posting_state: GlPostingState,
218 pub journal_id: Option<Uuid>,
219 pub accounting_post_id: Option<Uuid>,
220 pub posted_at: Option<DateTime<Utc>>,
221 pub notes: Option<String>,
222 pub metadata: AuditMetadata,
223}
224
225#[derive(Debug, Clone, Serialize)]
234#[cfg_attr(feature = "openapi", derive(ToSchema))]
235#[serde(rename_all = "camelCase")]
236pub struct DeliveryNoteListResponseDto {
237 pub items: Vec<DeliveryNoteResponseDto>,
239 pub total: u64,
241 pub page: u32,
243 pub per_page: u32,
245 pub total_pages: u32,
247 pub has_next: bool,
249 pub has_prev: bool,
251}
252
253impl DeliveryNoteListResponseDto {
254 pub fn new(items: Vec<DeliveryNoteResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
256 let total_pages = if per_page > 0 {
257 ((total as f64) / (per_page as f64)).ceil() as u32
258 } else {
259 0
260 };
261 Self {
262 items,
263 total,
264 page,
265 per_page,
266 total_pages,
267 has_next: page < total_pages,
268 has_prev: page > 1,
269 }
270 }
271}
272
273#[derive(Debug, Clone, Serialize)]
275#[cfg_attr(feature = "openapi", derive(ToSchema))]
276#[serde(rename_all = "camelCase")]
277pub struct DeliveryNoteSummaryDto {
278 pub id: Uuid,
279 pub delivery_number: String,
280 pub branch_id: Option<Uuid>,
281 pub customer_id: Uuid,
282 pub created_at: Option<DateTime<Utc>>,
283}
284
285impl From<DeliveryNote> for DeliveryNoteResponseDto {
290 fn from(entity: DeliveryNote) -> Self {
291 Self {
292 id: entity.id,
293 delivery_number: entity.delivery_number,
294 branch_id: entity.branch_id,
295 customer_id: entity.customer_id,
296 source_so_id: entity.source_so_id,
297 warehouse_id: entity.warehouse_id,
298 posting_date: entity.posting_date,
299 total_cogs: entity.total_cogs,
300 cogs_account_id: entity.cogs_account_id,
301 inventory_account_id: entity.inventory_account_id,
302 transfer_id: entity.transfer_id,
303 status: entity.status,
304 posting_state: entity.posting_state,
305 journal_id: entity.journal_id,
306 accounting_post_id: entity.accounting_post_id,
307 posted_at: entity.posted_at,
308 notes: entity.notes,
309 metadata: entity.metadata,
310 }
311 }
312}
313
314impl From<DeliveryNote> for DeliveryNoteSummaryDto {
315 fn from(entity: DeliveryNote) -> Self {
316 let created_at = backbone_core::PersistentEntity::created_at(&entity);
317 Self {
318 id: entity.id,
319 delivery_number: entity.delivery_number,
320 branch_id: entity.branch_id,
321 customer_id: entity.customer_id,
322 created_at,
323 }
324 }
325}
326
327impl From<CreateDeliveryNoteDto> for DeliveryNote {
328 fn from(dto: CreateDeliveryNoteDto) -> Self {
329 Self {
330 id: Uuid::new_v4(),
331 delivery_number: dto.delivery_number,
332 branch_id: dto.branch_id,
333 customer_id: dto.customer_id,
334 source_so_id: dto.source_so_id,
335 warehouse_id: dto.warehouse_id,
336 posting_date: dto.posting_date,
337 total_cogs: dto.total_cogs,
338 cogs_account_id: dto.cogs_account_id,
339 inventory_account_id: dto.inventory_account_id,
340 transfer_id: dto.transfer_id,
341 status: DocStatus::Draft,
342 posting_state: dto.posting_state,
343 journal_id: dto.journal_id,
344 accounting_post_id: dto.accounting_post_id,
345 posted_at: dto.posted_at,
346 notes: dto.notes,
347 metadata: AuditMetadata::default(),
348 }
349 }
350}
351
352impl From<&DeliveryNote> for DeliveryNoteResponseDto {
353 fn from(entity: &DeliveryNote) -> Self {
354 Self {
355 id: entity.id.clone(),
356 delivery_number: entity.delivery_number.clone(),
357 branch_id: entity.branch_id.clone(),
358 customer_id: entity.customer_id.clone(),
359 source_so_id: entity.source_so_id.clone(),
360 warehouse_id: entity.warehouse_id.clone(),
361 posting_date: entity.posting_date.clone(),
362 total_cogs: entity.total_cogs.clone(),
363 cogs_account_id: entity.cogs_account_id.clone(),
364 inventory_account_id: entity.inventory_account_id.clone(),
365 transfer_id: entity.transfer_id.clone(),
366 status: entity.status.clone(),
367 posting_state: entity.posting_state.clone(),
368 journal_id: entity.journal_id.clone(),
369 accounting_post_id: entity.accounting_post_id.clone(),
370 posted_at: entity.posted_at.clone(),
371 notes: entity.notes.clone(),
372 metadata: entity.metadata.clone(),
373 }
374 }
375}
376
377impl backbone_core::FromCreateDto<CreateDeliveryNoteDto> for DeliveryNote {
378 fn from_create_dto(dto: CreateDeliveryNoteDto) -> backbone_core::ServiceResult<Self> {
379 Ok(DeliveryNote::from(dto))
380 }
381}
382
383impl backbone_core::ApplyUpdateDto<UpdateDeliveryNoteDto> for DeliveryNote {
384 fn apply_update(mut self, dto: UpdateDeliveryNoteDto) -> backbone_core::ServiceResult<Self> {
385 self.delivery_number = dto.delivery_number;
386 self.branch_id = dto.branch_id;
387 self.customer_id = dto.customer_id;
388 self.source_so_id = dto.source_so_id;
389 self.warehouse_id = dto.warehouse_id;
390 self.posting_date = dto.posting_date;
391 self.total_cogs = dto.total_cogs;
392 self.cogs_account_id = dto.cogs_account_id;
393 self.inventory_account_id = dto.inventory_account_id;
394 self.transfer_id = dto.transfer_id;
395 self.status = dto.status;
396 self.posting_state = dto.posting_state;
397 self.journal_id = dto.journal_id;
398 self.accounting_post_id = dto.accounting_post_id;
399 self.posted_at = dto.posted_at;
400 self.notes = dto.notes;
401 Ok(self)
402 }
403}
404