diff --git a/src/db_api/pieces.rs b/src/db_api/pieces.rs index ff2fc45..19471f7 100644 --- a/src/db_api/pieces.rs +++ b/src/db_api/pieces.rs @@ -74,7 +74,7 @@ impl RawMaterial { pub async fn get_net_requirements( &self, con: &mut PgConnection, - ) -> sqlx::Result> { + ) -> sqlx::Result> { Ok(sqlx::query!( r#" SELECT COUNT(items.id) as quantity, transformations.date as date @@ -93,7 +93,7 @@ impl RawMaterial { .fetch_all(con) .await? .into_iter() - .fold(HashMap::new(), |mut map, row| { + .fold(BTreeMap::new(), |mut map, row| { map.insert( row.date.expect("selecting only non null"), row.quantity.expect("selecting only non null") as i32, diff --git a/src/scheduler/resource_planning.rs b/src/scheduler/resource_planning.rs index 499e63e..6e47376 100644 --- a/src/scheduler/resource_planning.rs +++ b/src/scheduler/resource_planning.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use sqlx::PgPool; use crate::db_api::{ @@ -8,116 +6,56 @@ use crate::db_api::{ use super::Scheduler; -struct QueryResults { - pub current_date: u32, - pub shipments: HashMap>, +struct DayVariantNeedsData { + pub due_date: i32, + pub net_req: i32, + pub variant: RawMaterial, + pub under_allocated: Vec, pub suppliers: Vec, - pub net_req: HashMap, - pub material_kind: RawMaterial, } -async fn query_needed_data( +async fn resolve_day_needs( pool: &PgPool, - variant: RawMaterial, -) -> anyhow::Result { - let mut conn = pool.acquire().await.unwrap(); - - let net_req = variant.get_net_requirements(&mut conn).await?; - let suppliers = Supplier::get_by_item_kind(variant, &mut conn).await?; - - let mut shipment_map = HashMap::new(); - for day in net_req.keys() { - let shipments = - Shipment::get_under_allocated(*day, variant, &mut conn).await?; - if !shipments.is_empty() { - shipment_map.insert(*day, shipments); - } - } - - let query_results = QueryResults { - current_date: Scheduler::get_date(), - shipments: shipment_map, - suppliers, - net_req, - material_kind: variant, - }; - - Ok(query_results) -} - -// TODO: Take warehouse capacity into account -// Test if underallocated shipments are being processed correctly -pub async fn resolve_material_needs( - variant: RawMaterial, - pool: PgPool, + mut needs: DayVariantNeedsData, ) -> anyhow::Result<()> { - tracing::info!("Processing {:?} needs", variant); - - // 1. Get net requirements for the variant by day, - // Get under alocated incomming shipments - // Get available suppliers - //TODO: resolve net requirements for one day at a time, starting from the - //earliest day. This will allow for better allocation of shipments - let qr = query_needed_data(&pool, variant).await?; - if qr.net_req.is_empty() { - tracing::info!("No {:#?} needs at the moment", variant); - return Ok(()); - } - - tracing::info!( - "Net {:#?} requirements ({{day: ammount}}): {:?}", - variant, - qr.net_req - ); - tracing::trace!("{:#?} suppliers: {:?}", variant, qr.suppliers); - tracing::trace!("Under allocated shipments: {:?}", qr.shipments); - // 2. Process the data to create purchase orders - let pr = process_purchases(qr); + let pr = process_purchases(&mut needs); - tracing::debug!( - "Altered shipments: {:#?}", - pr.altered_shipments_by_due_date - ); - tracing::debug!( - "New Purchase orders: {:#?}", - pr.purchase_orders_by_due_date - ); + tracing::debug!("Altered shipments: {:#?}", pr.altered_shipments); + tracing::debug!("New Purchase order: {:#?}", pr.purchase_order); // 3. Get pending items from database let mut tx = pool.begin().await?; - let mut pending = variant.get_pending_purchase(&mut tx).await?; + let mut pending = needs.variant.get_pending_purchase(&mut tx).await?; let mut material_shipments = Vec::::new(); //4. Link pending items to the altered existing shipments - for (due_date, shipments) in pr.altered_shipments_by_due_date { - for s in shipments { - let items_to_insert = pending + for ship in pr.altered_shipments { + let items_to_insert = pending + .iter() + .filter(|p| p.due_date == needs.due_date) + .take(ship.added as usize) + .map(|p| MaterialShipment::new(p.item_id, ship.id)) + .collect::>(); + + pending.retain(|p| { + !items_to_insert .iter() - .filter(|p| p.due_date == due_date) - .take(s.added as usize) - .map(|p| MaterialShipment::new(p.item_id, s.id)) - .collect::>(); - - pending.retain(|p| { - !items_to_insert - .iter() - .any(|i| i.raw_material_id() == p.item_id) - }); - - material_shipments.extend(items_to_insert); - } + .any(|i| i.raw_material_id() == p.item_id) + }); + + material_shipments.extend(items_to_insert); } // 5. Insert new shipments into de dabase to get their IDs // 6. Link remaining pending items to new purchase orders - for (due_date, po) in pr.purchase_orders_by_due_date { - let id = po.insert(&mut tx).await?; + if let Some(shipment) = pr.purchase_order { + let id = shipment.insert(&mut tx).await?; let items_to_insert = pending .iter() - .filter(|p| p.due_date == due_date) - .take(po.quantity() as usize) + .filter(|p| p.due_date == needs.due_date) + .take(needs.net_req as usize) .map(|p| MaterialShipment::new(p.item_id, id)) .collect::>(); @@ -137,14 +75,85 @@ pub async fn resolve_material_needs( tx.commit().await?; - tracing::info!("Resolved {:#?} needs", variant); + Ok(()) +} +struct QueryResults { + pub shipments: Vec, + pub suppliers: Vec, +} + +async fn query_needed_data( + pool: &PgPool, + variant: RawMaterial, + due_date: i32, +) -> anyhow::Result { + let mut conn = pool.acquire().await.unwrap(); + + let suppliers = Supplier::get_by_item_kind(variant, &mut conn).await?; + + let shipments = + Shipment::get_under_allocated(due_date, variant, &mut conn).await?; + + let query_results = QueryResults { + shipments, + suppliers, + }; + + Ok(query_results) +} + +// TODO: Take warehouse capacity into account +// Test if underallocated shipments are being processed correctly +pub async fn resolve_material_needs( + variant: RawMaterial, + pool: PgPool, +) -> anyhow::Result<()> { + tracing::info!("Processing {:?} needs", variant); + + let net_req = { + let mut conn = pool.acquire().await.unwrap(); + variant.get_net_requirements(&mut conn).await + }?; + + if net_req.is_empty() { + tracing::info!("No {:#?} needs at the moment", variant); + return Ok(()); + } + + tracing::info!( + "Net {:#?} requirements ({{day: ammount}}): {:?}", + variant, + net_req + ); + + for (day, quantity) in net_req.iter() { + // 1. Get net requirements for the variant by day, + // Get under alocated incomming shipments + // Get available suppliers + //TODO: resolve net requirements for one day at a time, starting from the + //earliest day. This will allow for better allocation of shipments + let qr = query_needed_data(&pool, variant, *day).await?; + tracing::trace!("{:#?} suppliers: {:?}", variant, qr.suppliers); + tracing::trace!("Under allocated shipments: {:?}", qr.shipments); + + let needs_data = DayVariantNeedsData { + due_date: *day, + net_req: *quantity, + variant, + under_allocated: qr.shipments, + suppliers: qr.suppliers, + }; + resolve_day_needs(&pool, needs_data).await?; + } + + tracing::info!("Resolved {:#?} needs", variant); Ok(()) } struct PurchaseProcessingResults { - pub purchase_orders_by_due_date: HashMap, - pub altered_shipments_by_due_date: HashMap>, + pub purchase_order: Option, + pub altered_shipments: Vec, } #[derive(Debug)] @@ -153,107 +162,89 @@ struct AlteredShipment { pub added: i64, } -fn process_purchases(mut qr: QueryResults) -> PurchaseProcessingResults { +fn process_purchases( + needs: &mut DayVariantNeedsData, +) -> PurchaseProcessingResults { // 1. Remove from net requirements the stock already ordered in the past process_under_alocated_shipments( - &mut qr.net_req, - &mut qr.shipments, - qr.material_kind, + &mut needs.net_req, + &mut needs.under_allocated, ); // 2. retain only days with net requirements // retain only under allocated shipments to which stock - // was allocated and need to be updated onnthe database - qr.net_req.retain(|_, quantity| *quantity > 0); - qr.shipments.retain(|_, shipments| !shipments.is_empty()); + // was allocated and need to be updated on the database tracing::trace!( "Net requirements after shipment adjusts: {:?}", - qr.net_req + needs.net_req ); - let altered_shipments = qr - .shipments + let altered_shipments = needs + .under_allocated .iter() - .map(|(day, shipments)| { - let altered = shipments - .iter() - .map(|s| AlteredShipment { - id: s.id, - added: s.added.expect("added is always Some"), - }) - .collect(); - (*day, altered) + .map(|s| AlteredShipment { + id: s.id, + added: s.added.expect("added is always Some"), }) - .collect(); + .collect::>(); // 3. Create a purchase order for each supplier for each day // Fill low demand days with extra to reach the minimum order quantity. - let mut purchase_orders = HashMap::new(); - for (due_date, quantity) in qr.net_req.iter() { - let day = *due_date; - let available_time = day - qr.current_date as i32; - - if available_time < 0 { - tracing::warn!("Material for day {} is already due", day); - continue; - } - - let suppliers = qr.suppliers.clone(); - let Some(cheapest_purchase) = suppliers - .into_iter() - .filter_map(|s| match s.can_deliver_in(available_time) { - true => Some(s.shipment(*quantity, *due_date)), - false => None, - }) - .min_by_key(|shipment| shipment.cost().0) - else { - tracing::warn!( - "No supplier can deliver {:#?} in time for day {}", - qr.material_kind, - day - ); - continue; + let current_date = Scheduler::get_date(); + let available_time = needs.due_date - current_date as i32; + + if available_time < 0 { + tracing::warn!("Material for day {} is already due", needs.due_date); + return PurchaseProcessingResults { + purchase_order: None, + altered_shipments, }; + } - purchase_orders.insert(day, cheapest_purchase); + let suppliers = needs.suppliers.clone(); + let cheapest_purchase = suppliers + .into_iter() + .filter_map(|s| match s.can_deliver_in(available_time) { + true => Some(s.shipment(needs.net_req, needs.due_date)), + false => None, + }) + .min_by_key(|shipment| shipment.cost().0); + + if cheapest_purchase.is_none() { + tracing::warn!( + "No supplier can deliver {:#?} in time for day {}", + needs.variant, + needs.due_date + ); } PurchaseProcessingResults { - purchase_orders_by_due_date: purchase_orders, - altered_shipments_by_due_date: altered_shipments, + purchase_order: cheapest_purchase, + altered_shipments, } } fn process_under_alocated_shipments( - net_req: &mut HashMap, - shipments: &mut HashMap>, - material_kind: RawMaterial, + net_req: &mut i32, + under_allocated: &mut Vec, ) { - for (day, quantity) in net_req.iter_mut() { - let Some(under_allocated) = shipments.get_mut(day) else { - continue; - }; - - for s in under_allocated.iter_mut() { - if *quantity == 0 { - break; - } - - let allocated = s.extra_quantity.min(*quantity as i64); - *quantity -= allocated as i32; - s.extra_quantity -= allocated; - s.added = Some(allocated); - - tracing::info!( - "Allocated {} free slot from shipment id {} for day {}'s {:#?} needs", - allocated, - s.id, - day, - material_kind - ); + for s in under_allocated.iter_mut() { + if *net_req == 0 { + break; } - // remove shipments to which nothing was allocated - under_allocated.retain(|s| s.added.is_some()); + let allocated = s.extra_quantity.min(*net_req as i64); + *net_req -= allocated as i32; + s.extra_quantity -= allocated; + s.added = Some(allocated); + + tracing::info!( + "Allocated {} free slot from shipment id {}", + allocated, + s.id, + ); } + + // remove shipments to which nothing was allocated + under_allocated.retain(|s| s.added.is_some()); }