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 77e630b26efdd5d483d382d5b3145ebf4a405ff0b63c969c495476d4f6ebab7d:


import clock
import database.{type Conn, type DbError, type Database}
import gleam/dict.{type Dict}
import gleam/int
import gleam/list
import gleam/option.{None, Some}
import gleam/order
import gleam/result
import gleam/string
import ql361api/models.{type PurchaseOrderItem}
import repo.{type Receipt, type ReceiptLine, Receipt, ReceiptLine}
import wisp

pub type LineInput {
  LineInput(
    bill_detail_id: Int,
    received_qty: Float,
    batch_no: String,
    produced_on: String,
    expires_on: String,
    location_code: String,
  )
}

pub type ReceiptError {
  ReceiptRejected(String)
  ReceiptDb(DbError)
}

pub fn normalize_line(input: LineInput) -> Result(LineInput, String) {
  use received_qty <- result.try(parse_qty(input.received_qty))
  use batch_no <- result.try(parse_batch(input.batch_no))
  use produced_on <- result.try(parse_day(input.produced_on))
  use expires_on <- result.try(parse_day(input.expires_on))
  let location_code = string.uppercase(string.trim(input.location_code))
  case location_code == "" {
    True -> Error("库位不能为空")
    False ->
      case string.compare(produced_on, expires_on) {
        order.Gt -> Error("保质期到期日不能早于生产日期")
        _ ->
          Ok(LineInput(
            ..input,
            received_qty: received_qty,
            batch_no: batch_no,
            produced_on: produced_on,
            expires_on: expires_on,
            location_code: location_code,
          ))
      }
  }
}

pub fn parse_qty(value: Float) -> Result(Float, String) {
  case value >. 0.0 {
    True -> Ok(value)
    False -> Error("实收数量必须大于 0")
  }
}

pub fn parse_batch(value: String) -> Result(String, String) {
  let value = string.trim(value)
  case value == "" || string.length(value) > 40 {
    True -> Error("批次号不能为空,且不超过 40 个字符")
    False -> Ok(value)
  }
}

pub fn parse_day(value: String) -> Result(String, String) {
  let value = string.trim(value)
  case string.length(value) == 10 && string.slice(value, 4, 1) == "-" && string.slice(value, 7, 1) == "-" {
    False -> Error("日期格式应为 YYYY-MM-DD")
    True ->
      case
        int.parse(string.slice(value, 0, 4)),
        int.parse(string.slice(value, 5, 2)),
        int.parse(string.slice(value, 8, 2))
      {
        Ok(year), Ok(month), Ok(day)
          if year >= 2000 && month >= 1 && month <= 12 && day >= 1 && day <= 31
        -> Ok(value)
        _, _, _ -> Error("日期格式应为 YYYY-MM-DD")
      }
  }
}

pub fn create(
  db: Database,
  bill_id: Int,
  inputs: List(LineInput),
) -> Result(Receipt, ReceiptError) {
  case inputs {
    [] -> Error(ReceiptRejected("至少要有一行收货明细"))
    _ -> {
      let created_at = clock.now_local()
      let receipt_id = compact(created_at) <> "-" <> wisp.random_string(8)
      case database.transaction(db, fn(conn) { post(conn, bill_id, receipt_id, created_at, inputs) }) {
        Ok(receipt) -> Ok(receipt)
        Error(database.DbError(message)) -> Error(classify(message))
      }
    }
  }
}

fn post(
  conn: Conn,
  bill_id: Int,
  receipt_id: String,
  created_at: String,
  inputs: List(LineInput),
) -> Result(Receipt, DbError) {
  use order <- result.try(repo.get_inbound(conn, bill_id))
  case order {
    None -> Error(database.DbError("采购订单不存在"))
    Some(order) -> {
      use warehouse <- result.try(repo.get_warehouse(conn, order.warehouse_id))
      case warehouse {
        None -> Error(database.DbError("采购订单的仓库不存在"))
        Some(row) ->
          case order.status == 20 && row.operational {
            False -> Error(database.DbError("只能对总仓等待收货的采购订单收货"))
            True ->
              case order.items {
                [] -> Error(database.DbError("采购订单明细尚未同步"))
                items -> {
                  use lines <- result.try(prepare_lines(
                    conn,
                    bill_id,
                    order.warehouse_id,
                    items,
                    inputs,
                    dict.new(),
                    [],
                  ))
                  let receipt =
                    Receipt(
                      id: receipt_id,
                      bill_id: bill_id,
                      warehouse_id: order.warehouse_id,
                      created_at: created_at,
                      lines: list.reverse(lines),
                    )
                  use _ <- result.try(repo.insert_receipt(conn, receipt))
                  use _ <- result.try(list.try_fold(receipt.lines, Nil, fn(_, line) {
                    repo.add_inventory(conn, receipt.warehouse_id, line, created_at)
                  }))
                  Ok(receipt)
                }
              }
          }
      }
    }
  }
}

fn prepare_lines(
  conn: Conn,
  bill_id: Int,
  warehouse_id: Int,
  items: List(PurchaseOrderItem),
  inputs: List(LineInput),
  seen: Dict(Int, Nil),
  acc: List(ReceiptLine),
) -> Result(List(ReceiptLine), DbError) {
  case inputs {
    [] -> Ok(acc)
    [input, ..rest] ->
      case normalize_line(input) {
        Error(message) -> Error(database.DbError(message))
        Ok(input) ->
          case dict.has_key(seen, input.bill_detail_id) {
            True -> Error(database.DbError("同一明细不能在一张收货单里重复"))
            False ->
              case list.find(items, fn(item) { item.bill_detail_id == input.bill_detail_id }) {
                Error(_) -> Error(database.DbError("明细不属于这张采购订单"))
                Ok(item) -> {
                  use spot <- result.try(repo.find_location(conn, warehouse_id, input.location_code))
                  case spot {
                    Some(spot) if spot.enabled -> {
                      use already <- result.try(repo.sum_received_qty(conn, bill_id, input.bill_detail_id))
                      case already +. input.received_qty <=. item.base_qty +. 0.0001 {
                        False -> Error(database.DbError("实收数量超过订单未收数量"))
                        True -> {
                          let line =
                            ReceiptLine(
                              line_no: list.length(acc) + 1,
                              bill_detail_id: input.bill_detail_id,
                              sku_id: item.sku_id,
                              goods_code: item.goods_code,
                              goods_name: item.goods_name,
                              barcode: item.barcode,
                              received_qty: input.received_qty,
                              batch_no: input.batch_no,
                              produced_on: input.produced_on,
                              expires_on: input.expires_on,
                              location_code: spot.code,
                            )
                          prepare_lines(
                            conn,
                            bill_id,
                            warehouse_id,
                            items,
                            rest,
                            dict.insert(seen, input.bill_detail_id, Nil),
                            [line, ..acc],
                          )
                        }
                      }
                    }
                    _ -> Error(database.DbError("库位不存在或未启用"))
                  }
                }
              }
          }
      }
  }
}

fn classify(message: String) -> ReceiptError {
  case
    message == "采购订单不存在"
    || message == "只能对总仓等待收货的采购订单收货"
    || message == "采购订单明细尚未同步"
    || string.starts_with(message, "实收")
    || string.starts_with(message, "库位")
    || string.starts_with(message, "批次")
    || string.starts_with(message, "日期")
    || string.starts_with(message, "保质")
    || string.starts_with(message, "明细")
    || string.starts_with(message, "同一")
  {
    True -> ReceiptRejected(message)
    False -> ReceiptDb(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.