use rust_decimal::Decimal;
use sqlx::{PgConnection, Row};
use uuid::Uuid;
pub struct MoveStateRow {
pub id: Uuid,
pub item_id: Uuid,
pub state: String,
pub demand_qty: Decimal,
pub quantity: Decimal,
pub is_inventory: bool,
}
pub struct TransferHeaderRow {
pub id: Uuid,
pub name: String,
pub origin: Option<String>,
pub picking_type_id: Uuid,
pub location_id: Uuid,
pub location_dest_id: Uuid,
pub state: String,
pub date_done: Option<chrono::DateTime<chrono::Utc>>,
}
pub struct OperationTypeFacts {
pub id: Uuid,
pub code: String,
pub reservation_method: String,
pub create_backorder: String,
pub sequence_code: String,
pub move_type: String,
}
pub struct AdjustmentMoveRow {
pub id: Uuid,
pub state: String,
pub date: Option<chrono::DateTime<chrono::Utc>>,
}
pub struct NewPickingRow<'a> {
pub id: Uuid,
pub name: &'a str,
pub origin: Option<&'a str>,
pub picking_type_id: Uuid,
pub location_id: Uuid,
pub location_dest_id: Uuid,
pub partner_id: Option<Uuid>,
pub move_type: &'a str,
}
#[derive(Default)]
pub struct StockPickingRepository;
impl StockPickingRepository {
pub fn new() -> Self {
Self
}
pub async fn insert_transfer(
&self,
conn: &mut PgConnection,
t: &NewPickingRow<'_>,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"INSERT INTO inventory.transfers
(id, name, origin, priority, picking_type_id, location_id, location_dest_id,
partner_id, move_type, scheduled_date, state)
VALUES ($1, $2, $3, 'normal', $4, $5, $6, $7, $8::move_type, NOW(),
'draft'::transfer_state)"#,
)
.bind(t.id)
.bind(t.name)
.bind(t.origin)
.bind(t.picking_type_id)
.bind(t.location_id)
.bind(t.location_dest_id)
.bind(t.partner_id)
.bind(t.move_type)
.execute(conn)
.await?;
Ok(())
}
pub async fn fetch_transfer(
&self,
conn: &mut PgConnection,
transfer_id: Uuid,
) -> Result<Option<TransferHeaderRow>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT id, name, origin, picking_type_id, location_id, location_dest_id,
state::text AS state, date_done
FROM inventory.transfers
WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(transfer_id)
.fetch_optional(conn)
.await?;
Ok(row.map(|r| TransferHeaderRow {
id: r.get("id"),
name: r.get("name"),
origin: r.get("origin"),
picking_type_id: r.get("picking_type_id"),
location_id: r.get("location_id"),
location_dest_id: r.get("location_dest_id"),
state: r.get("state"),
date_done: r.get("date_done"),
}))
}
pub async fn fetch_moves_of_transfer(
&self,
conn: &mut PgConnection,
transfer_id: Uuid,
) -> Result<Vec<MoveStateRow>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT id, item_id, state::text AS state, demand_qty, quantity, is_inventory
FROM inventory.stock_moves
WHERE picking_id = $1 AND (metadata->>'deleted_at') IS NULL
ORDER BY create_date, id"#,
)
.bind(transfer_id)
.fetch_all(conn)
.await?;
Ok(rows
.iter()
.map(|r| MoveStateRow {
id: r.get("id"),
item_id: r.get("item_id"),
state: r.get("state"),
demand_qty: r.get("demand_qty"),
quantity: r.get("quantity"),
is_inventory: r.get("is_inventory"),
})
.collect())
}
pub async fn move_ids_of_transfer(
&self,
conn: &mut PgConnection,
transfer_id: Uuid,
) -> Result<Vec<Uuid>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT id FROM inventory.stock_moves
WHERE picking_id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(transfer_id)
.fetch_all(conn)
.await?;
Ok(rows.iter().map(|r| r.get("id")).collect())
}
pub async fn fetch_operation_type(
&self,
conn: &mut PgConnection,
operation_type_id: Uuid,
) -> Result<Option<OperationTypeFacts>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT id, code::text AS code, reservation_method::text AS reservation_method,
create_backorder::text AS create_backorder, sequence_code,
move_type::text AS move_type
FROM inventory.operation_types
WHERE id = $1 AND active AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(operation_type_id)
.fetch_optional(conn)
.await?;
Ok(row.map(|r| OperationTypeFacts {
id: r.get("id"),
code: r.get("code"),
reservation_method: r.get("reservation_method"),
create_backorder: r.get("create_backorder"),
sequence_code: r.get("sequence_code"),
move_type: r.get("move_type"),
}))
}
pub async fn find_open_group_picking(
&self,
conn: &mut PgConnection,
picking_type_id: Uuid,
location_id: Uuid,
location_dest_id: Uuid,
partner_id: Option<Uuid>,
origin: Option<&str>,
) -> Result<Option<Uuid>, sqlx::Error> {
let row = sqlx::query_scalar::<_, Uuid>(
r#"SELECT id FROM inventory.transfers
WHERE picking_type_id = $1
AND location_id = $2
AND location_dest_id = $3
AND partner_id IS NOT DISTINCT FROM $4
AND origin IS NOT DISTINCT FROM $5
AND state NOT IN ('done'::transfer_state, 'cancel'::transfer_state)
AND (metadata->>'deleted_at') IS NULL
ORDER BY scheduled_date DESC, id
LIMIT 1"#,
)
.bind(picking_type_id)
.bind(location_id)
.bind(location_dest_id)
.bind(partner_id)
.bind(origin)
.fetch_optional(conn)
.await?;
Ok(row)
}
pub async fn resolve_internal_location(
&self,
conn: &mut PgConnection,
warehouse_id: Uuid,
) -> Result<Option<Uuid>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT id FROM inventory.locations
WHERE warehouse_id = $1 AND usage = 'internal'::location_usage AND active
AND (metadata->>'deleted_at') IS NULL
ORDER BY parent_path ASC NULLS FIRST, name ASC LIMIT 1"#,
)
.bind(warehouse_id)
.fetch_optional(conn)
.await?;
Ok(row.map(|r| r.get("id")))
}
pub async fn resolve_inventory_loss_location(
&self,
conn: &mut PgConnection,
) -> Result<Option<Uuid>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT id FROM inventory.locations
WHERE usage = 'inventory'::location_usage AND active
AND (metadata->>'deleted_at') IS NULL
ORDER BY name ASC LIMIT 1"#,
)
.fetch_optional(conn)
.await?;
Ok(row.map(|r| r.get("id")))
}
pub async fn ensure_internal_location(
&self,
conn: &mut PgConnection,
warehouse_id: Uuid,
) -> Result<Uuid, sqlx::Error> {
if let Some(id) = self.resolve_internal_location(conn, warehouse_id).await? {
return Ok(id);
}
let id = Uuid::new_v4();
sqlx::query(
r#"INSERT INTO inventory.locations
(id, name, complete_name, usage, active, warehouse_id, parent_path)
VALUES ($1, 'Stock', 'Stock', 'internal'::location_usage, TRUE, $2, $1::text)"#,
)
.bind(id)
.bind(warehouse_id)
.execute(conn)
.await?;
Ok(id)
}
pub async fn ensure_inventory_loss_location(
&self,
conn: &mut PgConnection,
) -> Result<Uuid, sqlx::Error> {
if let Some(id) = self.resolve_inventory_loss_location(conn).await? {
return Ok(id);
}
let id = Uuid::new_v4();
sqlx::query(
r#"INSERT INTO inventory.locations
(id, name, complete_name, usage, active, parent_path)
VALUES ($1, 'Inventory adjustment', 'Inventory adjustment',
'inventory'::location_usage, TRUE, $1::text)"#,
)
.bind(id)
.execute(conn)
.await?;
Ok(id)
}
pub async fn resolve_partner_location(
&self,
conn: &mut PgConnection,
usage: &str,
) -> Result<Option<Uuid>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT id FROM inventory.locations
WHERE usage = $1::location_usage AND active
AND (metadata->>'deleted_at') IS NULL
ORDER BY name ASC LIMIT 1"#,
)
.bind(usage)
.fetch_optional(conn)
.await?;
Ok(row.map(|r| r.get("id")))
}
pub async fn ensure_partner_location(
&self,
conn: &mut PgConnection,
usage: &str,
) -> Result<Uuid, sqlx::Error> {
if let Some(id) = self.resolve_partner_location(conn, usage).await? {
return Ok(id);
}
let id = Uuid::new_v4();
let name = if usage == "supplier" { "Suppliers" } else { "Customers" };
sqlx::query(
r#"INSERT INTO inventory.locations
(id, name, complete_name, usage, active, parent_path)
VALUES ($1, $2, $2, $3::location_usage, TRUE, $1::text)"#,
)
.bind(id)
.bind(name)
.bind(usage)
.execute(conn)
.await?;
Ok(id)
}
pub async fn ensure_quant_surface(
&self,
conn: &mut PgConnection,
item_id: Uuid,
location_id: Uuid,
warehouse_id: Option<Uuid>,
) -> Result<(), sqlx::Error> {
sqlx::query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))")
.bind(format!("quant:{item_id}:{location_id}:NULL:NULL:NULL"))
.execute(&mut *conn)
.await?;
sqlx::query(
r#"INSERT INTO inventory.stock_quants
(id, item_id, location_id, quantity, reserved_quantity, available_quantity,
in_date)
SELECT $1, $2, $3,
COALESCE((SELECT b.actual_qty FROM inventory.bins b
WHERE b.item_id = $2
AND b.warehouse_id = $4::uuid
AND (b.metadata->>'deleted_at') IS NULL), 0),
0,
COALESCE((SELECT b.actual_qty FROM inventory.bins b
WHERE b.item_id = $2
AND b.warehouse_id = $4::uuid
AND (b.metadata->>'deleted_at') IS NULL), 0),
NOW()
WHERE $4::uuid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM inventory.stock_quants q
WHERE q.item_id = $2 AND q.location_id = $3
AND q.lot_id IS NULL AND q.package_id IS NULL AND q.owner_id IS NULL
AND (q.metadata->>'deleted_at') IS NULL
)"#,
)
.bind(Uuid::new_v4())
.bind(item_id)
.bind(location_id)
.bind(warehouse_id)
.execute(&mut *conn)
.await?;
sqlx::query(
r#"INSERT INTO inventory.stock_quants
(id, item_id, location_id, quantity, reserved_quantity, available_quantity,
in_date)
SELECT $1, $2, $3, 0, 0, 0, NOW()
WHERE $4::uuid IS NULL
AND NOT EXISTS (
SELECT 1 FROM inventory.stock_quants q
WHERE q.item_id = $2 AND q.location_id = $3
AND q.lot_id IS NULL AND q.package_id IS NULL AND q.owner_id IS NULL
AND (q.metadata->>'deleted_at') IS NULL
)"#,
)
.bind(Uuid::new_v4())
.bind(item_id)
.bind(location_id)
.bind(warehouse_id)
.execute(&mut *conn)
.await?;
Ok(())
}
pub async fn find_adjustment_moves(
&self,
conn: &mut PgConnection,
item_id: Uuid,
location_a: Uuid,
location_b: Uuid,
) -> Result<Vec<AdjustmentMoveRow>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT id, state::text AS state, date
FROM inventory.stock_moves
WHERE item_id = $1 AND is_inventory
AND (metadata->>'deleted_at') IS NULL
AND ((location_id = $2 AND location_dest_id = $3)
OR (location_id = $3 AND location_dest_id = $2))
ORDER BY (state NOT IN ('done'::move_state, 'cancel'::move_state)) DESC,
date DESC NULLS LAST, create_date DESC"#,
)
.bind(item_id)
.bind(location_a)
.bind(location_b)
.fetch_all(conn)
.await?;
Ok(rows
.iter()
.map(|r| AdjustmentMoveRow { id: r.get("id"), state: r.get("state"), date: r.get("date") })
.collect())
}
}