Fosi Fossil Service
Small teams. Open source. Built on Fossil.

IaC / SwiftFlow

High-performance WMS with wave picking, smart routing, and real-time inventory visibility.

Public | Owner: IaC | Default branch: trunk | Last update: 2026-10-23 14:37

Artifact 29be83076a841e88887ef26c35f503e080d5ecd59bde622c9fdb46f07f35e298:


import clock
import database.{
  type Conn, type Database, type DbError, FloatParam, IntParam, TextParam,
}
import gleam/dict
import gleam/dynamic/decode
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/order
import gleam/result
import gleam/string
import ql361api/json_codec
import ql361api/models.{type SalesOrder, type SalesOrderItem}
import repo.{type InventoryBatch, InventoryBatch}
import shipping
import wisp

pub type WaveError {
  WaveRejected(String)
  WaveDb(DbError)
}

pub type Demand {
  Demand(
    bill_id: Int,
    bill_code: String,
    bill_detail_id: Int,
    sku_id: Int,
    goods_code: String,
    goods_name: String,
    barcode: String,
    spec: String,
    base_unit: String,
    qty: Float,
  )
}

pub type Allocation {
  Allocation(
    bill_id: Int,
    bill_code: String,
    bill_detail_id: Int,
    qty: Float,
  )
}

pub type PickLine {
  PickLine(
    line_no: Int,
    sku_id: Int,
    goods_code: String,
    goods_name: String,
    barcode: String,
    spec: String,
    base_unit: String,
    location_code: String,
    batch_no: String,
    produced_on: String,
    expires_on: String,
    qty: Float,
    short_qty: Float,
    allocations: List(Allocation),
  )
}

pub type WaveOrder {
  WaveOrder(bill_id: Int, bill_code: String, customer_name: String)
}

pub type WaveSummary {
  WaveSummary(
    id: String,
    warehouse_id: Int,
    warehouse_name: String,
    status: String,
    order_count: Int,
    pick_lines: Int,
    short_qty: Float,
    created_by: String,
    created_at: String,
  )
}

pub type Wave {
  Wave(
    id: String,
    warehouse_id: Int,
    warehouse_name: String,
    status: String,
    created_by: String,
    created_at: String,
    orders: List(WaveOrder),
    picks: List(PickLine),
  )
}

type Header {
  Header(
    id: String,
    warehouse_id: Int,
    status: String,
    created_by: String,
    created_at: String,
  )
}

/// 先按到期日从早到晚占用库存,再按库位编码排成一条拣货路线。缺货单独成行,不占用库位。
pub fn plan(
  demands: List(Demand),
  batches: List(InventoryBatch),
) -> List(PickLine) {
  group_demands(demands)
  |> list.flat_map(fn(group) { allocate_group(group, batches) })
  |> list.sort(by_stop)
  |> number(1, [])
}

pub fn create(
  db: Database,
  bill_ids: List(Int),
  created_by: String,
  warehouse_id: Int,
) -> Result(Wave, WaveError) {
  case bill_ids {
    [] -> Error(WaveRejected("至少选择一张订单"))
    ids ->
      case list.length(ids) > 50 {
        True -> Error(WaveRejected("一次最多组成 50 张订单"))
        False ->
          case list.length(ids) != list.length(list.unique(ids)) {
            True -> Error(WaveRejected("订单重复"))
            False -> {
              let created_at = clock.now_local()
              let wave_id =
                "WV-" <> compact(created_at) <> "-" <> wisp.random_string(4)
              case
                database.transaction(db, fn(conn) {
                  build(conn, wave_id, ids, created_by, created_at, warehouse_id)
                })
              {
                Ok(wave) -> Ok(wave)
                Error(database.DbError(message)) -> Error(classify(message))
              }
            }
          }
      }
  }
}

pub fn cancel(
  db: Database,
  wave_id: String,
  warehouse_id: Int,
) -> Result(Wave, WaveError) {
  let cancelled_at = clock.now_local()
  case
    database.transaction(db, fn(conn) {
      use found <- result.try(read_header(conn, wave_id))
      case found {
        None -> Error(database.DbError("波次不存在"))
        Some(header) ->
          case header.warehouse_id == warehouse_id {
            False -> Error(database.DbError("单据不属于当前作业仓库"))
            True ->
              case header.status == "open" {
                False -> Error(database.DbError("只能撤销未完成的波次"))
                True -> {
                  use _ <- result.try(database.exec_on(
                    conn,
                    "UPDATE waves SET status = 'cancelled', cancelled_at = ?
                     WHERE id = ? AND status = 'open'",
                    [TextParam(cancelled_at), TextParam(wave_id)],
                  ))
                  read_wave(conn, wave_id)
                }
              }
          }
      }
    })
  {
    Ok(wave) -> Ok(wave)
    Error(database.DbError(message)) -> Error(classify(message))
  }
}

pub fn get(db: Database, wave_id: String) -> Result(Wave, WaveError) {
  case database.transaction(db, fn(conn) { read_wave(conn, wave_id) }) {
    Ok(wave) -> Ok(wave)
    Error(database.DbError(message)) -> Error(classify(message))
  }
}

pub fn list(
  db: Database,
  warehouse_id: Int,
  limit: Int,
  offset: Int,
) -> Result(#(Int, List(WaveSummary)), WaveError) {
  case
    database.transaction(db, fn(conn) {
      list_rows(conn, warehouse_id, limit, offset)
    })
  {
    Ok(page) -> Ok(page)
    Error(database.DbError(message)) -> Error(classify(message))
  }
}

fn build(
  conn: Conn,
  wave_id: String,
  bill_ids: List(Int),
  created_by: String,
  created_at: String,
  station: Int,
) -> Result(Wave, DbError) {
  use loaded <- result.try(load_orders(conn, bill_ids, []))
  let orders =
    list.sort(loaded, fn(left: SalesOrder, right: SalesOrder) {
      case string.compare(left.bill_date, right.bill_date) {
        order.Eq -> int.compare(left.bill_id, right.bill_id)
        compared -> compared
      }
    })
  use warehouse_id <- result.try(same_station(orders, station))
  use stored <- result.try(repo.get_warehouse(conn, warehouse_id))
  case stored {
    None -> Error(database.DbError("销售订单的仓库不存在"))
    Some(stored) ->
      case stored.operational {
        False -> Error(database.DbError("只能把总仓待发货的订单组成波次"))
        True -> {
          use demands <- result.try(collect_demands(conn, orders, []))
          use stock <- result.try(repo.list_inventory(conn, warehouse_id))
          use reserved <- result.try(list_reserved(conn, warehouse_id))
          let picks = plan(demands, apply_reserved(stock, reserved))
          let wave_orders =
            list.map(orders, fn(order: SalesOrder) {
              WaveOrder(
                bill_id: order.bill_id,
                bill_code: order.bill_code,
                customer_name: order.customer_name,
              )
            })
          let wave =
            Wave(
              id: wave_id,
              warehouse_id: warehouse_id,
              warehouse_name: stored.warehouse.fullname,
              status: "open",
              created_by: created_by,
              created_at: created_at,
              orders: wave_orders,
              picks: picks,
            )
          use _ <- result.try(insert_wave(conn, wave))
          Ok(wave)
        }
      }
  }
}

fn load_orders(
  conn: Conn,
  ids: List(Int),
  acc: List(SalesOrder),
) -> Result(List(SalesOrder), DbError) {
  case ids {
    [] -> Ok(acc)
    [id, ..rest] -> {
      use found <- result.try(repo.get_order(conn, id))
      case found {
        None -> Error(database.DbError("销售订单不存在"))
        Some(order) -> load_orders(conn, rest, [order, ..acc])
      }
    }
  }
}

fn same_station(orders: List(SalesOrder), station: Int) -> Result(Int, DbError) {
  case orders {
    [] -> Error(database.DbError("至少选择一张订单"))
    [first, ..] ->
      case list.all(orders, fn(order: SalesOrder) { order.warehouse_id == first.warehouse_id }) {
        False -> Error(database.DbError("订单不属于同一仓库"))
        True ->
          case first.warehouse_id == station {
            True -> Ok(first.warehouse_id)
            False -> Error(database.DbError("单据不属于当前作业仓库"))
          }
      }
  }
}

fn collect_demands(
  conn: Conn,
  orders: List(SalesOrder),
  acc: List(Demand),
) -> Result(List(Demand), DbError) {
  case orders {
    [] -> Ok(acc)
    [order, ..rest] -> {
      use rows <- result.try(demands_of(conn, order))
      collect_demands(conn, rest, list.append(acc, rows))
    }
  }
}

fn demands_of(conn: Conn, order: SalesOrder) -> Result(List(Demand), DbError) {
  use locked <- result.try(order_locked(conn, order.bill_id))
  case locked {
    True -> Error(database.DbError("订单已在未完成的波次里"))
    False ->
      case order.status == 20 {
        False -> Error(database.DbError("只能把总仓待发货的订单组成波次"))
        True ->
          case order.items {
            [] -> Error(database.DbError("订单明细尚未同步"))
            items -> {
              use rows <- result.try(remaining_lines(conn, order, items, []))
              case rows {
                [] -> Error(database.DbError("订单已发完"))
                _ -> Ok(list.reverse(rows))
              }
            }
          }
      }
  }
}

fn remaining_lines(
  conn: Conn,
  order: SalesOrder,
  items: List(SalesOrderItem),
  acc: List(Demand),
) -> Result(List(Demand), DbError) {
  case items {
    [] -> Ok(acc)
    [item, ..rest] -> {
      use shipped <- result.try(repo.sum_shipped_qty(conn, order.bill_id, item.bill_detail_id))
      let left = item.base_qty -. shipped
      let acc = case left >. 0.0001 {
        False -> acc
        True -> [
          Demand(
            bill_id: order.bill_id,
            bill_code: order.bill_code,
            bill_detail_id: item.bill_detail_id,
            sku_id: item.sku_id,
            goods_code: item.goods_code,
            goods_name: item.goods_name,
            barcode: item.barcode,
            spec: item.spec,
            base_unit: item.base_unit,
            qty: left,
          ),
          ..acc
        ]
      }
      remaining_lines(conn, order, rest, acc)
    }
  }
}

fn order_locked(conn: Conn, bill_id: Int) -> Result(Bool, DbError) {
  use rows <- result.try(database.query_on(
    conn,
    "SELECT COUNT(*) FROM wave_orders wo
     JOIN waves w ON w.id = wo.wave_id
     WHERE wo.bill_id = ? AND w.status = 'open'",
    [IntParam(bill_id)],
    {
      use count <- decode.field(0, json_codec.int())
      decode.success(count)
    },
  ))
  case rows {
    [count, ..] -> Ok(count > 0)
    [] -> Ok(False)
  }
}

fn list_reserved(conn: Conn, warehouse_id: Int) -> Result(List(InventoryBatch), DbError) {
  database.query_on(
    conn,
    "SELECT location_code, sku_id, goods_code, batch_no, produced_on, expires_on,
            SUM(qty)
     FROM wave_picks p
     JOIN waves w ON w.id = p.wave_id
     WHERE w.warehouse_id = ? AND w.status = 'open' AND p.qty > 0
     GROUP BY location_code, sku_id, goods_code, batch_no, produced_on, expires_on",
    [IntParam(warehouse_id)],
    {
      use location_code <- decode.field(0, decode.string)
      use sku_id <- decode.field(1, json_codec.int())
      use goods_code <- decode.field(2, decode.string)
      use batch_no <- decode.field(3, decode.string)
      use produced_on <- decode.field(4, decode.string)
      use expires_on <- decode.field(5, decode.string)
      use qty <- decode.field(6, json_codec.number())
      decode.success(InventoryBatch(
        warehouse_id: warehouse_id,
        location_code: location_code,
        sku_id: sku_id,
        goods_code: goods_code,
        batch_no: batch_no,
        produced_on: produced_on,
        expires_on: expires_on,
        qty: qty,
      ))
    },
  )
}

fn apply_reserved(
  batches: List(InventoryBatch),
  reserved: List(InventoryBatch),
) -> List(InventoryBatch) {
  list.filter_map(batches, fn(batch: InventoryBatch) {
    let held =
      list.fold(reserved, 0.0, fn(acc, row: InventoryBatch) {
        case same_slot(batch, row) {
          True -> acc +. row.qty
          False -> acc
        }
      })
    let qty = batch.qty -. held
    case qty >. 0.0001 {
      True -> Ok(InventoryBatch(..batch, qty: qty))
      False -> Error(Nil)
    }
  })
}

fn same_slot(left: InventoryBatch, right: InventoryBatch) -> Bool {
  left.location_code == right.location_code
  && left.sku_id == right.sku_id
  && left.goods_code == right.goods_code
  && left.batch_no == right.batch_no
  && left.produced_on == right.produced_on
  && left.expires_on == right.expires_on
}

fn group_demands(demands: List(Demand)) -> List(List(Demand)) {
  let #(keys, groups) =
    list.fold(demands, #([], dict.new()), fn(acc, demand: Demand) {
      let #(keys, groups) = acc
      let key = stock_key(demand)
      case dict.get(groups, key) {
        Ok(rows) -> #(keys, dict.insert(groups, key, list.append(rows, [demand])))
        Error(_) -> #(list.append(keys, [key]), dict.insert(groups, key, [demand]))
      }
    })
  list.filter_map(keys, fn(key) { dict.get(groups, key) })
}

fn allocate_group(
  demands: List(Demand),
  batches: List(InventoryBatch),
) -> List(PickLine) {
  case demands {
    [] -> []
    [first, ..] -> {
      let matched = list.filter(batches, fn(batch) { same_goods(batch, first) })
      let need = list.fold(demands, 0.0, fn(acc, demand: Demand) { acc +. demand.qty })
      let available = list.fold(matched, 0.0, fn(acc, batch: InventoryBatch) { acc +. batch.qty })
      let take = case available <. need {
        True -> available
        False -> need
      }
      let picks = case take >. 0.0001 {
        False -> []
        True ->
          case shipping.allocate(take, matched) {
            Ok(#(picks, _)) -> picks
            Error(_) -> []
          }
      }
      let picked = list.fold(picks, 0.0, fn(acc, pick: shipping.Pick) { acc +. pick.qty })
      let short = need -. picked
      let #(lines, left) =
        list.fold(picks, #([], demands), fn(acc, pick: shipping.Pick) {
          let #(lines, pending) = acc
          let #(allocs, pending) = take_alloc(pick.qty, pending, [])
          #([from_pick(first, pick, allocs), ..lines], pending)
        })
      let short_lines = case short >. 0.0001 {
        False -> []
        True -> [from_short(first, left, short)]
      }
      list.append(list.reverse(lines), short_lines)
    }
  }
}

fn from_pick(
  sample: Demand,
  pick: shipping.Pick,
  allocations: List(Allocation),
) -> PickLine {
  PickLine(
    line_no: 0,
    sku_id: sample.sku_id,
    goods_code: sample.goods_code,
    goods_name: sample.goods_name,
    barcode: sample.barcode,
    spec: sample.spec,
    base_unit: sample.base_unit,
    location_code: pick.location_code,
    batch_no: pick.batch_no,
    produced_on: pick.produced_on,
    expires_on: pick.expires_on,
    qty: pick.qty,
    short_qty: 0.0,
    allocations: allocations,
  )
}

fn from_short(sample: Demand, pending: List(Demand), short: Float) -> PickLine {
  let #(allocations, _) = take_alloc(short, pending, [])
  PickLine(
    line_no: 0,
    sku_id: sample.sku_id,
    goods_code: sample.goods_code,
    goods_name: sample.goods_name,
    barcode: sample.barcode,
    spec: sample.spec,
    base_unit: sample.base_unit,
    location_code: "",
    batch_no: "",
    produced_on: "",
    expires_on: "",
    qty: 0.0,
    short_qty: short,
    allocations: allocations,
  )
}

fn take_alloc(
  need: Float,
  demands: List(Demand),
  acc: List(Allocation),
) -> #(List(Allocation), List(Demand)) {
  case need <=. 0.0001 {
    True -> #(list.reverse(acc), demands)
    False ->
      case demands {
        [] -> #(list.reverse(acc), [])
        [demand, ..rest] ->
          case demand.qty <=. 0.0001 {
            True -> take_alloc(need, rest, acc)
            False -> {
              let taken = case demand.qty <=. need {
                True -> demand.qty
                False -> need
              }
              let allocation =
                Allocation(
                  bill_id: demand.bill_id,
                  bill_code: demand.bill_code,
                  bill_detail_id: demand.bill_detail_id,
                  qty: taken,
                )
              take_alloc(
                need -. taken,
                [Demand(..demand, qty: demand.qty -. taken), ..rest],
                [allocation, ..acc],
              )
            }
          }
      }
  }
}

fn same_goods(batch: InventoryBatch, demand: Demand) -> Bool {
  case demand.sku_id == 0 {
    True -> batch.goods_code == demand.goods_code
    False -> batch.sku_id == demand.sku_id
  }
}

fn stock_key(demand: Demand) -> String {
  case demand.sku_id == 0 {
    True -> "code:" <> demand.goods_code
    False -> "sku:" <> int.to_string(demand.sku_id)
  }
}

fn by_stop(left: PickLine, right: PickLine) -> order.Order {
  case left.location_code == "", right.location_code == "" {
    True, False -> order.Gt
    False, True -> order.Lt
    _, _ ->
      case string.compare(left.location_code, right.location_code) {
        order.Eq ->
          case string.compare(left.expires_on, right.expires_on) {
            order.Eq ->
              case string.compare(left.batch_no, right.batch_no) {
                order.Eq -> string.compare(left.goods_code, right.goods_code)
                compared -> compared
              }
            compared -> compared
          }
        compared -> compared
      }
  }
}

fn number(lines: List(PickLine), n: Int, acc: List(PickLine)) -> List(PickLine) {
  case lines {
    [] -> list.reverse(acc)
    [line, ..rest] -> number(rest, n + 1, [PickLine(..line, line_no: n), ..acc])
  }
}

fn insert_wave(conn: Conn, wave: Wave) -> Result(Nil, DbError) {
  use _ <- result.try(database.exec_on(
    conn,
    "INSERT INTO waves (id, warehouse_id, status, created_by, created_at, cancelled_at)
     VALUES (?, ?, ?, ?, ?, '')",
    [
      TextParam(wave.id),
      IntParam(wave.warehouse_id),
      TextParam(wave.status),
      TextParam(wave.created_by),
      TextParam(wave.created_at),
    ],
  ))
  use _ <- result.try(list.try_fold(wave.orders, Nil, fn(_, order: WaveOrder) {
    database.exec_on(
      conn,
      "INSERT INTO wave_orders (wave_id, bill_id, bill_code, customer_name)
       VALUES (?, ?, ?, ?)",
      [
        TextParam(wave.id),
        IntParam(order.bill_id),
        TextParam(order.bill_code),
        TextParam(order.customer_name),
      ],
    )
  }))
  list.try_fold(wave.picks, Nil, fn(_, line: PickLine) {
    use _ <- result.try(database.exec_on(
      conn,
      "INSERT INTO wave_picks (
         wave_id, line_no, sku_id, goods_code, goods_name, barcode, spec, base_unit,
         location_code, batch_no, produced_on, expires_on, qty, short_qty
       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
      [
        TextParam(wave.id),
        IntParam(line.line_no),
        IntParam(line.sku_id),
        TextParam(line.goods_code),
        TextParam(line.goods_name),
        TextParam(line.barcode),
        TextParam(line.spec),
        TextParam(line.base_unit),
        TextParam(line.location_code),
        TextParam(line.batch_no),
        TextParam(line.produced_on),
        TextParam(line.expires_on),
        FloatParam(line.qty),
        FloatParam(line.short_qty),
      ],
    ))
    list.try_fold(line.allocations, Nil, fn(_, allocation: Allocation) {
      database.exec_on(
        conn,
        "INSERT INTO wave_allocations (wave_id, line_no, bill_id, bill_code, bill_detail_id, qty)
         VALUES (?, ?, ?, ?, ?, ?)",
        [
          TextParam(wave.id),
          IntParam(line.line_no),
          IntParam(allocation.bill_id),
          TextParam(allocation.bill_code),
          IntParam(allocation.bill_detail_id),
          FloatParam(allocation.qty),
        ],
      )
    })
  })
}

fn read_wave(conn: Conn, wave_id: String) -> Result(Wave, DbError) {
  use found <- result.try(read_header(conn, wave_id))
  case found {
    None -> Error(database.DbError("波次不存在"))
    Some(header) -> {
      use stored <- result.try(repo.get_warehouse(conn, header.warehouse_id))
      let warehouse_name = case stored {
        Some(row) -> row.warehouse.fullname
        None -> ""
      }
      use orders <- result.try(database.query_on(
        conn,
        "SELECT bill_id, bill_code, customer_name
         FROM wave_orders WHERE wave_id = ? ORDER BY bill_id",
        [TextParam(wave_id)],
        {
          use bill_id <- decode.field(0, json_codec.int())
          use bill_code <- decode.field(1, decode.string)
          use customer_name <- decode.field(2, decode.string)
          decode.success(WaveOrder(
            bill_id: bill_id,
            bill_code: bill_code,
            customer_name: customer_name,
          ))
        },
      ))
      use picks <- result.try(database.query_on(
        conn,
        "SELECT line_no, sku_id, goods_code, goods_name, barcode, spec, base_unit,
                location_code, batch_no, produced_on, expires_on, qty, short_qty
         FROM wave_picks WHERE wave_id = ? ORDER BY line_no",
        [TextParam(wave_id)],
        {
          use line_no <- decode.field(0, json_codec.int())
          use sku_id <- decode.field(1, json_codec.int())
          use goods_code <- decode.field(2, decode.string)
          use goods_name <- decode.field(3, decode.string)
          use barcode <- decode.field(4, decode.string)
          use spec <- decode.field(5, decode.string)
          use base_unit <- decode.field(6, decode.string)
          use location_code <- decode.field(7, decode.string)
          use batch_no <- decode.field(8, decode.string)
          use produced_on <- decode.field(9, decode.string)
          use expires_on <- decode.field(10, decode.string)
          use qty <- decode.field(11, json_codec.number())
          use short_qty <- decode.field(12, json_codec.number())
          decode.success(PickLine(
            line_no: line_no,
            sku_id: sku_id,
            goods_code: goods_code,
            goods_name: goods_name,
            barcode: barcode,
            spec: spec,
            base_unit: base_unit,
            location_code: location_code,
            batch_no: batch_no,
            produced_on: produced_on,
            expires_on: expires_on,
            qty: qty,
            short_qty: short_qty,
            allocations: [],
          ))
        },
      ))
      use allocations <- result.try(database.query_on(
        conn,
        "SELECT line_no, bill_id, bill_code, bill_detail_id, qty
         FROM wave_allocations
         WHERE wave_id = ?
         ORDER BY line_no, bill_id, bill_detail_id",
        [TextParam(wave_id)],
        {
          use line_no <- decode.field(0, json_codec.int())
          use bill_id <- decode.field(1, json_codec.int())
          use bill_code <- decode.field(2, decode.string)
          use bill_detail_id <- decode.field(3, json_codec.int())
          use qty <- decode.field(4, json_codec.number())
          decode.success(#(
            line_no,
            Allocation(
              bill_id: bill_id,
              bill_code: bill_code,
              bill_detail_id: bill_detail_id,
              qty: qty,
            ),
          ))
        },
      ))
      let picks =
        list.map(picks, fn(line: PickLine) {
          let rows =
            list.filter_map(allocations, fn(row) {
              let #(line_no, allocation) = row
              case line_no == line.line_no {
                True -> Ok(allocation)
                False -> Error(Nil)
              }
            })
          PickLine(..line, allocations: rows)
        })
      Ok(Wave(
        id: header.id,
        warehouse_id: header.warehouse_id,
        warehouse_name: warehouse_name,
        status: header.status,
        created_by: header.created_by,
        created_at: header.created_at,
        orders: orders,
        picks: picks,
      ))
    }
  }
}

fn read_header(conn: Conn, wave_id: String) -> Result(Option(Header), DbError) {
  use rows <- result.try(database.query_on(
    conn,
    "SELECT id, warehouse_id, status, created_by, created_at
     FROM waves WHERE id = ?",
    [TextParam(wave_id)],
    {
      use id <- decode.field(0, decode.string)
      use warehouse_id <- decode.field(1, json_codec.int())
      use status <- decode.field(2, decode.string)
      use created_by <- decode.field(3, decode.string)
      use created_at <- decode.field(4, decode.string)
      decode.success(Header(
        id: id,
        warehouse_id: warehouse_id,
        status: status,
        created_by: created_by,
        created_at: created_at,
      ))
    },
  ))
  case rows {
    [] -> Ok(None)
    [header, ..] -> Ok(Some(header))
  }
}

fn list_rows(
  conn: Conn,
  warehouse_id: Int,
  limit: Int,
  offset: Int,
) -> Result(#(Int, List(WaveSummary)), DbError) {
  use totals <- result.try(database.query_on(
    conn,
    "SELECT COUNT(*) FROM waves WHERE warehouse_id = ?",
    [IntParam(warehouse_id)],
    {
      use total <- decode.field(0, json_codec.int())
      decode.success(total)
    },
  ))
  let total = case totals {
    [value, ..] -> value
    [] -> 0
  }
  use rows <- result.try(database.query_on(
    conn,
    "SELECT w.id, w.warehouse_id, COALESCE(wh.name, ''), w.status,
            (SELECT COUNT(*) FROM wave_orders o WHERE o.wave_id = w.id),
            (SELECT COUNT(*) FROM wave_picks p WHERE p.wave_id = w.id AND p.qty > 0.0001),
            (SELECT COALESCE(SUM(p.short_qty), 0) FROM wave_picks p WHERE p.wave_id = w.id),
            w.created_by, w.created_at
     FROM waves w
     LEFT JOIN warehouses wh ON wh.ql_id = w.warehouse_id
     WHERE w.warehouse_id = ?
     ORDER BY CASE WHEN w.status = 'open' THEN 0 ELSE 1 END, w.created_at DESC, w.id DESC
     LIMIT ? OFFSET ?",
    [IntParam(warehouse_id), IntParam(limit), IntParam(offset)],
    {
      use id <- decode.field(0, decode.string)
      use warehouse_id <- decode.field(1, json_codec.int())
      use warehouse_name <- decode.field(2, decode.string)
      use status <- decode.field(3, decode.string)
      use order_count <- decode.field(4, json_codec.int())
      use pick_lines <- decode.field(5, json_codec.int())
      use short_qty <- decode.field(6, json_codec.number())
      use created_by <- decode.field(7, decode.string)
      use created_at <- decode.field(8, decode.string)
      decode.success(WaveSummary(
        id: id,
        warehouse_id: warehouse_id,
        warehouse_name: warehouse_name,
        status: status,
        order_count: order_count,
        pick_lines: pick_lines,
        short_qty: short_qty,
        created_by: created_by,
        created_at: created_at,
      ))
    },
  ))
  Ok(#(total, rows))
}

fn classify(message: String) -> WaveError {
  case
    message == "至少选择一张订单"
    || message == "一次最多组成 50 张订单"
    || message == "订单重复"
    || message == "销售订单不存在"
    || message == "订单不属于同一仓库"
    || message == "单据不属于当前作业仓库"
    || message == "销售订单的仓库不存在"
    || message == "只能把总仓待发货的订单组成波次"
    || message == "订单明细尚未同步"
    || message == "订单已发完"
    || message == "订单已在未完成的波次里"
    || message == "波次不存在"
    || message == "只能撤销未完成的波次"
  {
    True -> WaveRejected(message)
    False -> WaveDb(database.DbError(message))
  }
}

fn compact(value: String) -> String {
  value
  |> string.replace("-", "")
  |> string.replace(" ", "")
  |> string.replace(":", "")
}

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/IaC/SwiftFlow

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.