use rust_decimal::Decimal;
use sqlx::{PgConnection, PgPool, Row};
use uuid::Uuid;
use backbone_orm::org_scope;
use crate::domain::entity::StockMove;
use super::picking_batch_projection_repository::PickingBatchProjectionRepository;
pub const TABLE_NAME: &str = "inventory.stock_moves";
pub struct StockMoveRepository(
backbone_orm::GenericCrudRepository<StockMove, backbone_orm::SoftDelete>,
);
impl std::ops::Deref for StockMoveRepository {
type Target = backbone_orm::GenericCrudRepository<StockMove, backbone_orm::SoftDelete>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl StockMoveRepository {
pub fn new(pool: PgPool) -> Self {
Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
}
}
pub struct NewMoveRow<'a> {
pub id: Uuid,
pub name: &'a str,
pub item_id: Uuid,
pub demand_qty: Decimal,
pub price_unit: Decimal,
pub procure_method: &'a str,
pub picking_id: Option<Uuid>,
pub origin: Option<&'a str>,
pub location_id: Uuid,
pub location_dest_id: Uuid,
pub partner_id: Option<Uuid>,
pub warehouse_id: Option<Uuid>,
pub orderpoint_id: Option<Uuid>,
pub move_orig_ids: Vec<Uuid>,
pub move_dest_ids: Vec<Uuid>,
pub is_inventory: bool,
pub scrapped: bool,
pub forced_value: Option<Decimal>,
}
#[derive(Clone)]
pub struct MoveRow {
pub id: Uuid,
pub name: String,
pub state: String,
pub posting_state: String,
pub date: chrono::DateTime<chrono::Utc>,
pub item_id: Uuid,
pub demand_qty: Decimal,
pub quantity: Decimal,
pub price_unit: Decimal,
pub procure_method: String,
pub picking_id: Option<Uuid>,
pub origin: Option<String>,
pub rule_id: Option<Uuid>,
pub location_id: Uuid,
pub location_dest_id: Uuid,
pub partner_id: Option<Uuid>,
pub warehouse_id: Option<Uuid>,
pub orderpoint_id: Option<Uuid>,
pub move_orig_ids: Vec<Uuid>,
pub move_dest_ids: Vec<Uuid>,
pub is_inventory: bool,
pub scrapped: bool,
pub propagate_cancel: bool,
pub forced_value: Option<Decimal>,
}
impl StockMoveRepository {
pub async fn insert_move(
&self,
conn: &mut sqlx::PgConnection,
m: &NewMoveRow<'_>,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"INSERT INTO inventory.stock_moves
(id, name, state, item_id, demand_qty, price_unit, procure_method, picking_id,
origin, location_id, location_dest_id, partner_id, warehouse_id,
orderpoint_id, move_orig_ids, move_dest_ids, is_inventory, scrapped, forced_value)
VALUES ($1,$2,'draft'::move_state,$3,$4,$5,$6::procure_method,$7,$8,$9,$10,$11,
$12,$13,$14,$15,$16,$17,$18)"#,
)
.bind(m.id).bind(m.name).bind(m.item_id).bind(m.demand_qty).bind(m.price_unit)
.bind(m.procure_method).bind(m.picking_id).bind(m.origin)
.bind(m.location_id).bind(m.location_dest_id).bind(m.partner_id)
.bind(m.warehouse_id).bind(m.orderpoint_id)
.bind(&m.move_orig_ids).bind(&m.move_dest_ids)
.bind(m.is_inventory).bind(m.scrapped).bind(m.forced_value)
.execute(conn)
.await?;
Ok(())
}
pub async fn fetch_move(
&self,
conn: &mut sqlx::PgConnection,
move_id: Uuid,
) -> Result<Option<MoveRow>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT id, name, state::text AS state, posting_state::text AS posting_state, date, item_id, demand_qty, quantity,
price_unit, procure_method::text AS procure_method, picking_id, origin, rule_id,
location_id, location_dest_id, partner_id, warehouse_id,
orderpoint_id, move_orig_ids, move_dest_ids, is_inventory, scrapped,
propagate_cancel, forced_value
FROM inventory.stock_moves
WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(move_id)
.fetch_optional(&mut *conn)
.await?;
Ok(row.map(move_row_of))
}
pub async fn fetch_moves(
&self,
conn: &mut sqlx::PgConnection,
move_ids: &[Uuid],
) -> Result<Vec<MoveRow>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT id, name, state::text AS state, posting_state::text AS posting_state, date, item_id, demand_qty, quantity,
price_unit, procure_method::text AS procure_method, picking_id, origin, rule_id,
location_id, location_dest_id, partner_id, warehouse_id,
orderpoint_id, move_orig_ids, move_dest_ids, is_inventory, scrapped,
propagate_cancel, forced_value
FROM inventory.stock_moves
WHERE id = ANY($1) AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(move_ids)
.fetch_all(&mut *conn)
.await?;
Ok(rows.into_iter().map(move_row_of).collect())
}
pub async fn fetch_moves_by_origin(
&self,
conn: &mut sqlx::PgConnection,
origin: &str,
) -> Result<Vec<MoveRow>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT id, name, state::text AS state, posting_state::text AS posting_state, date, item_id, demand_qty, quantity,
price_unit, procure_method::text AS procure_method, picking_id, origin, rule_id,
location_id, location_dest_id, partner_id, warehouse_id,
orderpoint_id, move_orig_ids, move_dest_ids, is_inventory, scrapped,
propagate_cancel, forced_value
FROM inventory.stock_moves
WHERE origin=$1 AND (metadata->>'deleted_at') IS NULL
ORDER BY name"#,
)
.bind(origin)
.fetch_all(&mut *conn)
.await?;
Ok(rows.into_iter().map(move_row_of).collect())
}
pub async fn transition_state(
&self,
conn: &mut sqlx::PgConnection,
move_id: Uuid,
expected_from: &str,
to: &str,
) -> Result<bool, sqlx::Error> {
let res = sqlx::query(
r#"UPDATE inventory.stock_moves
SET state = $3::move_state
WHERE id=$1 AND state = $2::move_state AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(move_id).bind(expected_from).bind(to)
.execute(&mut *conn)
.await?;
Ok(res.rows_affected() == 1)
}
pub async fn mark_done(
&self,
conn: &mut sqlx::PgConnection,
move_id: Uuid,
expected_from: &str,
done_qty: Decimal,
when: chrono::DateTime<chrono::Utc>,
) -> Result<bool, sqlx::Error> {
let res = sqlx::query(
r#"UPDATE inventory.stock_moves
SET state='done'::move_state, quantity=$3, date=$4
WHERE id=$1 AND state = $2::move_state AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(move_id).bind(expected_from).bind(done_qty).bind(when)
.execute(&mut *conn)
.await?;
Ok(res.rows_affected() == 1)
}
pub async fn set_picking(
&self,
conn: &mut sqlx::PgConnection,
move_id: Uuid,
picking_id: Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE inventory.stock_moves
SET picking_id = $2
WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(move_id)
.bind(picking_id)
.execute(&mut *conn)
.await?;
Ok(())
}
pub async fn link_chain(
&self,
conn: &mut sqlx::PgConnection,
parent_id: Uuid,
child_id: Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE inventory.stock_moves
SET move_dest_ids = array_append(move_dest_ids, $2)
WHERE id=$1 AND NOT ($2 = ANY(move_dest_ids))"#,
)
.bind(parent_id).bind(child_id)
.execute(&mut *conn)
.await?;
sqlx::query(
r#"UPDATE inventory.stock_moves
SET move_orig_ids = array_append(move_orig_ids, $2)
WHERE id=$1 AND NOT ($2 = ANY(move_orig_ids))"#,
)
.bind(child_id).bind(parent_id)
.execute(&mut *conn)
.await?;
Ok(())
}
}
impl StockMoveRepository {
pub async fn set_posting_pending(
&self,
conn: &mut PgConnection,
move_id: Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE inventory.stock_moves
SET posting_state='pending'::gl_posting_state
WHERE id=$1"#,
)
.bind(move_id)
.execute(&mut *conn)
.await?;
Ok(())
}
pub async fn mark_posting_posted(
&self,
pool: &PgPool,
move_id: Uuid,
) -> Result<(), sqlx::Error> {
org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE inventory.stock_moves
SET posting_state='posted'::gl_posting_state
WHERE id=$1 AND posting_state <> 'posted'::gl_posting_state"#,
)
.bind(move_id),
)
.await?;
Ok(())
}
pub async fn mark_posting_failed(
&self,
pool: &PgPool,
move_id: Uuid,
) -> Result<(), sqlx::Error> {
org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE inventory.stock_moves
SET posting_state='failed'::gl_posting_state
WHERE id=$1"#,
)
.bind(move_id),
)
.await?;
Ok(())
}
pub async fn mark_posting_not_applicable(
&self,
pool: &PgPool,
move_id: Uuid,
) -> Result<(), sqlx::Error> {
org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE inventory.stock_moves
SET posting_state='not_applicable'::gl_posting_state
WHERE id=$1 AND posting_state IN ('pending'::gl_posting_state, 'failed'::gl_posting_state)"#,
)
.bind(move_id),
)
.await?;
Ok(())
}
}
pub struct LocationFacts {
pub id: Uuid,
pub usage: String,
pub warehouse_id: Option<Uuid>,
pub valuation_account_id: Option<Uuid>,
}
impl StockMoveRepository {
pub async fn fetch_move_locations(
&self,
conn: &mut sqlx::PgConnection,
location_id: Uuid,
location_dest_id: Uuid,
) -> Result<(Option<LocationFacts>, Option<LocationFacts>), sqlx::Error> {
let rows = sqlx::query(
r#"SELECT id, usage::text AS usage, warehouse_id, valuation_account_id
FROM inventory.locations
WHERE id IN ($1, $2) AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(location_id)
.bind(location_dest_id)
.fetch_all(&mut *conn)
.await?;
let find = |id: Uuid| {
rows.iter().find(|r| r.get::<Uuid, _>("id") == id).map(|r| LocationFacts {
id: r.get("id"),
usage: r.get("usage"),
warehouse_id: r.get("warehouse_id"),
valuation_account_id: r.get("valuation_account_id"),
})
};
Ok((find(location_id), find(location_dest_id)))
}
}
impl StockMoveRepository {
pub async fn reproject_picking(
&self,
conn: &mut sqlx::PgConnection,
picking_id: Uuid,
) -> Result<Option<String>, sqlx::Error> {
let row = sqlx::query(
r#"WITH agg AS (
SELECT
COUNT(*) FILTER (WHERE state <> 'cancel'::move_state) AS live_count,
COUNT(*) AS total_count,
MIN(CASE state WHEN 'draft'::move_state THEN 0
WHEN 'waiting'::move_state THEN 1
WHEN 'confirmed'::move_state THEN 2
WHEN 'partially_available'::move_state THEN 2
WHEN 'assigned'::move_state THEN 3
WHEN 'done'::move_state THEN 4
ELSE 5 END) FILTER (WHERE state <> 'cancel'::move_state) AS rank
FROM inventory.stock_moves
WHERE picking_id = $1 AND (metadata->>'deleted_at') IS NULL
)
UPDATE inventory.transfers t
SET state = CASE agg.rank
WHEN 0 THEN 'draft'::transfer_state
WHEN 1 THEN 'waiting'::transfer_state
WHEN 2 THEN 'confirmed'::transfer_state
WHEN 3 THEN 'assigned'::transfer_state
WHEN 4 THEN 'done'::transfer_state
ELSE 'cancel'::transfer_state END,
date_done = CASE WHEN agg.rank = 4 THEN COALESCE(t.date_done, NOW())
ELSE t.date_done END
FROM agg
WHERE t.id = $1
RETURNING t.state::text"#,
)
.bind(picking_id)
.fetch_optional(&mut *conn)
.await?;
if row.is_some() {
PickingBatchProjectionRepository::default()
.reproject_batch_of_picking(conn, picking_id)
.await?;
}
Ok(row.map(|r| r.get::<String, _>("state")))
}
}
fn move_row_of(r: sqlx::postgres::PgRow) -> MoveRow {
MoveRow {
id: r.get("id"),
name: r.get("name"),
state: r.get("state"),
posting_state: r.get("posting_state"),
date: r.get("date"),
item_id: r.get("item_id"),
demand_qty: r.get("demand_qty"),
quantity: r.get("quantity"),
price_unit: r.get("price_unit"),
procure_method: r.get("procure_method"),
picking_id: r.get("picking_id"),
origin: r.get("origin"),
rule_id: r.get("rule_id"),
location_id: r.get("location_id"),
location_dest_id: r.get("location_dest_id"),
partner_id: r.get("partner_id"),
warehouse_id: r.get("warehouse_id"),
orderpoint_id: r.get("orderpoint_id"),
move_orig_ids: r.get("move_orig_ids"),
move_dest_ids: r.get("move_dest_ids"),
is_inventory: r.get("is_inventory"),
scrapped: r.get("scrapped"),
propagate_cancel: r.get("propagate_cancel"),
forced_value: r.get("forced_value"),
}
}
backbone_core::impl_crud_repository!(StockMoveRepository, StockMove, soft_delete);