Artifact 3bab1ab80d0e4c62f14a984455d6ed5a33f52ac2a244b5e1bf917518ffe96aa1:
- File
backend/src/router.gleam
— part of check-in
[bee9efb316]
at
2026-10-23 06:37:04
on branch trunk
— 同一仓库内移库,并留下波次已占用的数量
- 把一个批次从源库位转到启用的目标库位,库存立刻变更 - 未完成波次占用的数量不能移走,撤销波次后才能继续移 - 移库记录留在本地,不写回来肯 (user: pluto size: 65572)
import app.{type Context} import access.{type Principal} import clock import config.{Postgres, Sqlite} import database import gleam/dict import gleam/dynamic/decode import gleam/float import gleam/http.{type Method, Get, Post, Put} import gleam/http/request import gleam/int import gleam/json import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import location.{type Location, Location} import location_plan import picking import putaway import ql361api.{ type GoodsItem, type PurchaseOrder, type PurchaseOrderItem, type PurchaseOrderSummary, type SalesOrder, type SalesOrderItem, type SalesOrderSummary, } import ql361api/json_codec import rbac import receiving import repo import shipping import stock_move import session import sync_inbounds import sync_orders import sync_purchase_receipts import sync_products import sync_report import user_repo import wave import wisp.{type Request, type Response} pub fn handle_request(req: Request, ctx: Context) -> Response { use req <- wisp.handle_head(req) case wisp.path_segments(req) { [] -> only(req, Get, fn() { home_page(ctx) }) ["api", "health"] -> only(req, Get, fn() { health(ctx) }) ["api", "auth", "login"] -> only(req, Post, fn() { login(req, ctx) }) segments -> case access.authenticate(ctx.db, bearer_token(req)) { Error(err) -> auth_error(err) Ok(p) -> protected(req, ctx, p, segments) } } } fn protected( req: Request, ctx: Context, p: Principal, segments: List(String), ) -> Response { case segments { ["api", "auth", "me"] -> only(req, Get, fn() { me(p) }) ["api", "auth", "logout"] -> only(req, Post, fn() { logout(ctx, p) }) ["api", "auth", "warehouse"] -> only(req, Post, fn() { select_warehouse(req, ctx, p) }) ["api", "auth", "password"] -> only(req, Post, fn() { change_password(req, ctx, p) }) ["api", "users"] -> { use <- need(p, rbac.user_manage) case req.method { Get -> list_users(ctx) Post -> create_user(req, ctx, p) _ -> json_error(405, "方法不允许") } } ["api", "users", user_id] -> { use <- need(p, rbac.user_manage) only(req, Put, fn() { update_user(req, ctx, p, user_id) }) } ["api", "users", user_id, "password"] -> { use <- need(p, rbac.user_manage) only(req, Post, fn() { reset_password(req, ctx, p, user_id) }) } ["api", "roles"] -> { use <- need(p, rbac.user_manage) only(req, Get, fn() { list_roles(ctx) }) } ["api", "audit-logs"] -> { use <- need(p, rbac.audit_view) only(req, Get, fn() { list_audit(req, ctx) }) } ["api", "ql361", "status"] -> { use <- need(p, rbac.sync_view) only(req, Get, fn() { ql361_status(ctx) }) } ["api", "ql361", "login"] -> { use <- need(p, rbac.sync_run) only(req, Post, fn() { ql361_login(ctx) }) } ["api", "sync", "status"] -> { use <- need(p, rbac.sync_view) only(req, Get, fn() { sync_status(ctx) }) } ["api", "sync", "products"] -> { use <- need(p, rbac.sync_run) only(req, Post, fn() { sync_products_now(req, ctx, p) }) } ["api", "sync", "orders"] -> { use <- need(p, rbac.sync_run) only(req, Post, fn() { sync_orders_now(ctx, p) }) } ["api", "sync", "inbounds"] -> { use <- need(p, rbac.sync_run) only(req, Post, fn() { sync_inbounds_now(ctx, p) }) } ["api", "sync", "purchase-receipts"] -> { use <- need(p, rbac.sync_run) only(req, Post, fn() { sync_purchase_receipts_now(ctx, p) }) } ["api", "warehouses"] -> { use <- need(p, rbac.warehouse_view) only(req, Get, fn() { list_warehouses(ctx, p) }) } ["api", "locations"] -> case req.method { Get -> { use <- need(p, rbac.location_view) list_locations(req, ctx, p) } Post -> { use <- need(p, rbac.location_manage) create_location(req, ctx, p) } _ -> json_error(405, "方法不允许") } ["api", "locations", "plan", "preview"] -> { use <- need(p, rbac.location_manage) only(req, Post, fn() { plan_locations(req, ctx, p, False) }) } ["api", "locations", "plan"] -> { use <- need(p, rbac.location_manage) only(req, Post, fn() { plan_locations(req, ctx, p, True) }) } ["api", "locations", "disable"] -> { use <- need(p, rbac.location_manage) only(req, Post, fn() { disable_location(req, ctx, p) }) } ["api", "products"] -> { use <- need(p, rbac.product_view) only(req, Get, fn() { list_products(req, ctx) }) } ["api", "orders", "pending"] -> { use <- need(p, rbac.order_view) only(req, Get, fn() { list_orders(req, ctx, p, Some(20)) }) } ["api", "orders", "all"] -> { use <- need(p, rbac.order_view) only(req, Get, fn() { list_orders(req, ctx, p, None) }) } ["api", "orders", "detail", bill_id] -> { use <- need(p, rbac.order_view) only(req, Get, fn() { order_detail(ctx, p, bill_id) }) } ["api", "orders", bill_id, "shipments"] -> case req.method { Get -> { use <- need(p, rbac.order_view) list_shipments(ctx, p, bill_id) } Post -> { use <- need(p, rbac.shipment_execute) create_shipment(req, ctx, p, bill_id) } _ -> json_error(405, "方法不允许") } ["api", "inbounds", "pending"] -> { use <- need(p, rbac.inbound_view) only(req, Get, fn() { list_inbounds(req, ctx, p, 504, Some(20)) }) } ["api", "purchase-receipts"] -> { use <- need(p, rbac.inbound_view) only(req, Get, fn() { list_inbounds(req, ctx, p, 501, Some(20)) }) } ["api", "purchase-receipts", "detail", bill_id] -> { use <- need(p, rbac.inbound_view) only(req, Get, fn() { purchase_receipt_detail(ctx, p, bill_id) }) } ["api", "batches", batch_code] -> { use <- need(p, rbac.inbound_view) only(req, Get, fn() { batch_lookup(ctx, p, batch_code) }) } ["api", "putaways", "recommend"] -> { use <- need(p, rbac.putaway_execute) only(req, Post, fn() { recommend_putaway(req, ctx, p) }) } ["api", "putaways"] -> { use <- need(p, rbac.putaway_execute) only(req, Post, fn() { create_putaway(req, ctx, p) }) } ["api", "inbounds", "detail", bill_id] -> { use <- need(p, rbac.inbound_view) only(req, Get, fn() { inbound_detail(ctx, p, bill_id) }) } ["api", "inbounds", bill_id, "receipts"] -> case req.method { Get -> { use <- need(p, rbac.inbound_view) list_receipts(ctx, p, bill_id) } Post -> { use <- need(p, rbac.receiving_execute) create_receipt(req, ctx, p, bill_id) } _ -> json_error(405, "方法不允许") } ["api", "inventory"] -> { use <- need(p, rbac.inventory_view) only(req, Get, fn() { list_inventory(req, ctx, p) }) } ["api", "moves"] -> case req.method { Get -> { use <- need(p, rbac.move_execute) list_moves(req, ctx, p) } Post -> { use <- need(p, rbac.move_execute) create_move(req, ctx, p) } _ -> json_error(405, "方法不允许") } ["api", "picking", "from-order", bill_id] -> { use <- need(p, rbac.picking_execute) only(req, Get, fn() { picking_task(ctx, p, bill_id) }) } ["api", "waves", "candidates"] -> { use <- need(p, rbac.order_view) only(req, Get, fn() { wave_candidates(req, ctx, p) }) } ["api", "waves"] -> case req.method { Get -> { use <- need(p, rbac.order_view) list_waves(req, ctx, p) } Post -> { use <- need(p, rbac.wave_manage) create_wave(req, ctx, p) } _ -> json_error(405, "方法不允许") } ["api", "waves", wave_id, "cancel"] -> { use <- need(p, rbac.wave_manage) only(req, Post, fn() { cancel_wave(ctx, p, wave_id) }) } ["api", "waves", wave_id] -> { use <- need(p, rbac.order_view) only(req, Get, fn() { wave_detail(ctx, p, wave_id) }) } _ -> wisp.not_found() } } fn only(req: Request, method: Method, handler: fn() -> Response) -> Response { case req.method == method { True -> handler() False -> json_error(405, "方法不允许") } } fn need(p: Principal, permission: String, handler: fn() -> Response) -> Response { case access.has(p, permission) { True -> handler() False -> json_error(403, "没有权限执行该操作") } } fn in_scope(p: Principal, warehouse_id: Int, handler: fn() -> Response) -> Response { case rbac.allows(p.scope, warehouse_id) { True -> handler() False -> json_error(403, "无权访问该仓库") } } /// 写库存的作业只能在当前作业仓库进行,防止跨仓串单。 fn at_station(p: Principal, warehouse_id: Int, handler: fn() -> Response) -> Response { use <- in_scope(p, warehouse_id) case p.current_warehouse { 0 -> json_error(409, "请先选择当前作业仓库") current if current == warehouse_id -> handler() _ -> json_error(409, "单据不属于当前作业仓库") } } fn bearer_token(req: Request) -> String { case request.get_header(req, "authorization") { Error(_) -> "" Ok(value) -> case string.split_once(string.trim(value), " ") { Ok(#(scheme, token)) -> case string.lowercase(scheme) { "bearer" -> string.trim(token) _ -> "" } Error(_) -> "" } } } fn auth_error(err: access.AuthError) -> Response { case err { access.Unauthorized(message) -> json_error(401, message) access.Forbidden(message) -> json_error(403, message) access.Rejected(message) -> json_error(409, message) access.NotFound(message) -> json_error(404, message) access.AuthDb(database.DbError(message)) -> json_error(500, message) } } fn login(req: Request, ctx: Context) -> Response { use body <- wisp.require_json(req) let decoder = { use username <- decode.field("username", decode.string) use secret <- decode.field("password", decode.string) use terminal <- decode.field("terminal", decode.string) decode.success(#(username, secret, terminal)) } case decode.run(body, decoder) { Error(_) -> json_error(400, "需要 username、password、terminal") Ok(#(username, secret, terminal)) -> case access.login(ctx.db, ctx.auth, username, secret, terminal) { Error(err) -> auth_error(err) Ok(#(token, p)) -> json_ok([ #("token", json.string(token)), #("expires_at", json.string(p.expires_at)), #("user", principal_json(p)), ]) } } } fn me(p: Principal) -> Response { json_ok([#("user", principal_json(p))]) } fn logout(ctx: Context, p: Principal) -> Response { case access.logout(ctx.db, p) { Error(err) -> auth_error(err) Ok(_) -> json_ok([#("success", json.bool(True))]) } } fn select_warehouse(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) case decode.run(body, decode.at(["warehouse_id"], json_codec.int())) { Error(_) -> json_error(400, "需要 warehouse_id") Ok(warehouse_id) -> case access.select_warehouse(ctx.db, p, warehouse_id) { Error(err) -> auth_error(err) Ok(p) -> json_ok([#("user", principal_json(p))]) } } } fn change_password(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) let decoder = { use old <- decode.field("old_password", decode.string) use new <- decode.field("new_password", decode.string) decode.success(#(old, new)) } case decode.run(body, decoder) { Error(_) -> json_error(400, "需要 old_password、new_password") Ok(#(old, new)) -> case access.change_password(ctx.db, p, old, new) { Error(err) -> auth_error(err) Ok(_) -> json_ok([#("success", json.bool(True))]) } } } fn list_users(ctx: Context) -> Response { case access.list_users(ctx.db) { Error(err) -> auth_error(err) Ok(users) -> json_ok([#("users", json.array(users, of: user_json))]) } } fn create_user(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) let decoder = { use username <- decode.field("username", decode.string) use secret <- decode.field("password", decode.string) use input <- decode.then(user_input_decoder()) decode.success(#(username, secret, input)) } case decode.run(body, decoder) { Error(_) -> json_error(400, "需要 username、password、display_name、roles、warehouse_ids") Ok(#(username, secret, input)) -> case access.create_user(ctx.db, p, username, secret, input) { Error(err) -> auth_error(err) Ok(user) -> json_ok([#("user", user_json(user))]) } } } fn update_user(req: Request, ctx: Context, p: Principal, user_id: String) -> Response { use body <- wisp.require_json(req) case decode.run(body, user_input_decoder()) { Error(_) -> json_error(400, "需要 display_name、roles、warehouse_ids") Ok(input) -> case access.update_user(ctx.db, p, user_id, input) { Error(err) -> auth_error(err) Ok(user) -> json_ok([#("user", user_json(user))]) } } } fn reset_password(req: Request, ctx: Context, p: Principal, user_id: String) -> Response { use body <- wisp.require_json(req) case decode.run(body, decode.at(["password"], decode.string)) { Error(_) -> json_error(400, "需要 password") Ok(secret) -> case access.reset_password(ctx.db, p, user_id, secret) { Error(err) -> auth_error(err) Ok(_) -> json_ok([#("success", json.bool(True))]) } } } fn user_input_decoder() -> decode.Decoder(access.UserInput) { use display_name <- decode.field("display_name", decode.string) use enabled <- decode.optional_field("enabled", True, decode.bool) use roles <- decode.field("roles", decode.list(decode.string)) use warehouse_ids <- decode.field("warehouse_ids", decode.list(json_codec.int())) decode.success(access.UserInput( display_name: display_name, enabled: enabled, roles: roles, warehouse_ids: warehouse_ids, )) } fn list_roles(ctx: Context) -> Response { case database.transaction(ctx.db, user_repo.list_roles) { Error(database.DbError(message)) -> json_error(500, message) Ok(roles) -> { let names = rbac.permissions() |> list.map(fn(permission) { #(permission.code, permission.name) }) json_ok([ #( "roles", json.array(roles, of: fn(role: user_repo.RoleRow) { json.object([ #("code", json.string(role.code)), #("name", json.string(role.name)), #("terminals", json.array(role.terminals, of: json.string)), #("permissions", json.array(role.permissions, of: json.string)), ]) }), ), #( "permissions", json.array(names, of: fn(pair) { json.object([ #("code", json.string(pair.0)), #("name", json.string(pair.1)), ]) }), ), ]) } } } fn list_audit(req: Request, ctx: Context) -> Response { let limit = query_limit(req) let offset = query_offset(req) case database.transaction(ctx.db, fn(conn) { user_repo.list_audit(conn, limit, offset) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(#(total, entries)) -> json_ok([ #("total", json.int(total)), #("limit", json.int(limit)), #("offset", json.int(offset)), #( "logs", json.array(entries, of: fn(entry: user_repo.AuditEntry) { json.object([ #("id", json.string(entry.id)), #("user_id", json.string(entry.user_id)), #("username", json.string(entry.username)), #("action", json.string(entry.action)), #("target", json.string(entry.target)), #("warehouse_id", json.int(entry.warehouse_id)), #("detail", json.string(entry.detail)), #("created_at", json.string(entry.created_at)), ]) }), ), ]) } } fn principal_json(p: Principal) -> json.Json { json.object([ #("id", json.string(p.user_id)), #("username", json.string(p.username)), #("display_name", json.string(p.display_name)), #("roles", json.array(p.roles, of: json.string)), #("permissions", json.array(p.permissions, of: json.string)), #("all_warehouses", json.bool(p.scope == rbac.AllWarehouses)), #("warehouse_ids", json.array(p.warehouse_ids, of: json.int)), #("current_warehouse_id", json.int(p.current_warehouse)), #("terminal", json.string(p.terminal)), #("expires_at", json.string(p.expires_at)), ]) } fn user_json(user: user_repo.User) -> json.Json { json.object([ #("id", json.string(user.id)), #("username", json.string(user.username)), #("display_name", json.string(user.display_name)), #("enabled", json.bool(user.enabled)), #("roles", json.array(user.roles, of: json.string)), #("warehouse_ids", json.array(user.warehouse_ids, of: json.int)), #("created_at", json.string(user.created_at)), #("updated_at", json.string(user.updated_at)), ]) } fn audit_sync(ctx: Context, p: Principal, report: sync_report.SyncReport) -> Nil { access.audit( ctx.db, p, "sync." <> report.entity, report.entity, 0, "拉取 " <> int.to_string(report.fetched) <> ",写入 " <> int.to_string(report.upserted), ) } fn home_page(ctx: Context) -> Response { json_ok([ #("system", json.string("SwiftFlow WMS")), #("status", json.string("running")), #("database", json.string(driver_name(ctx))), ]) } fn health(ctx: Context) -> Response { let probe = database.transaction(ctx.db, fn(conn) { database.query_on(conn, "SELECT 1", [], { use value <- decode.field(0, json_codec.int()) decode.success(value) }) }) case probe { Ok(_) -> json_ok([ #("healthy", json.bool(True)), #("database", json.string(driver_name(ctx))), ]) Error(database.DbError(message)) -> wisp.json_response( json.to_string( json.object([ #("healthy", json.bool(False)), #("error", json.string(message)), ]), ), 500, ) } } fn ql361_status(ctx: Context) -> Response { let runs = read_runs(ctx) json_ok([ #( "credentials_configured", json.bool( ctx.config.ql361_username != "" && ctx.config.ql361_password != "", ), ), #("ql361_username", json.string(ctx.config.ql361_username)), #("session_cached", json.bool(session.cached(ctx.session))), #("base_url", json.string(ctx.config.ql361_base_url)), #("order_poll_seconds", json.int(ctx.config.order_poll_ms / 1000)), #("request_gap_ms", json.int(ctx.config.request_gap_ms)), #("order_begin_date", json.string(ctx.config.order_begin_date)), #( "stock_warehouses", json.array(ctx.config.stock_warehouses, of: json.string), ), #("database", json.string(driver_name(ctx))), #("recent_runs", runs_json(runs)), ]) } fn ql361_login(ctx: Context) -> Response { case session.ensure(ctx.session) { Ok(#(_, info)) -> json_ok([ #("success", json.bool(True)), #("company_name", json.string(info.company_name)), #("agent_real_name", json.string(info.agent_real_name)), ]) Error(err) -> json_error(400, ql361api.describe_ql361_error(err)) } } fn sync_status(ctx: Context) -> Response { json_ok([#("runs", runs_json(read_runs(ctx)))]) } fn sync_products_now(req: Request, ctx: Context, p: Principal) -> Response { case product_mode(req) { Error(message) -> json_error(400, message) Ok(mode) -> sync_response(ctx, p, sync_products.run(ctx, "manual", mode)) } } fn sync_purchase_receipts_now(ctx: Context, p: Principal) -> Response { sync_response(ctx, p, sync_purchase_receipts.run(ctx, "manual")) } fn sync_inbounds_now(ctx: Context, p: Principal) -> Response { sync_response(ctx, p, sync_inbounds.run(ctx, "manual")) } fn sync_orders_now(ctx: Context, p: Principal) -> Response { sync_response(ctx, p, sync_orders.run(ctx, "manual")) } fn sync_response( ctx: Context, p: Principal, outcome: Result(sync_report.SyncReport, sync_report.SyncError), ) -> Response { case outcome { Ok(report) -> { audit_sync(ctx, p, report) json_ok(report_json(report)) } Error(sync_report.SyncBusy) -> json_error(409, sync_report.describe(sync_report.SyncBusy)) Error(err) -> json_error(502, sync_report.describe(err)) } } fn list_warehouses(ctx: Context, p: Principal) -> Response { case database.transaction(ctx.db, repo.list_warehouses) { Error(database.DbError(message)) -> json_error(500, message) Ok(warehouses) -> json_ok([ #( "warehouses", warehouses |> list.filter(fn(row: repo.StoredWarehouse) { rbac.allows(p.scope, row.warehouse.id) }) |> json.array(of: fn(row: repo.StoredWarehouse) { json.object([ #("id", json.int(row.warehouse.id)), #("code", json.string(row.warehouse.code)), #("name", json.string(row.warehouse.fullname)), #("easycode", json.string(row.warehouse.easycode)), #("operational", json.bool(row.operational)), ]) }), ), ]) } } fn list_locations(req: Request, ctx: Context, p: Principal) -> Response { case int.parse(query_value(req, "warehouse_id", "")) { Error(_) -> json_error(400, "需要 warehouse_id") Ok(warehouse_id) -> { use <- in_scope(p, warehouse_id) case database.transaction(ctx.db, fn(conn) { use warehouse <- result.try(repo.get_warehouse(conn, warehouse_id)) use rows <- result.try(repo.list_locations(conn, warehouse_id)) Ok(#(warehouse, rows)) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(#(None, _)) -> json_error(404, "仓库不存在") Ok(#(Some(warehouse), rows)) -> json_ok([ #("warehouse_id", json.int(warehouse_id)), #("operational", json.bool(warehouse.operational)), #("locations", json.array(rows, of: location_json)), ]) } } } } fn create_location(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) case decode.run(body, location_input_decoder()) { Error(_) -> json_error(400, "库位参数无法解析") Ok(#(warehouse_id, area, aisle, level, bin)) -> { use <- in_scope(p, warehouse_id) case location.build(area, aisle, level, bin) { Error(message) -> json_error(400, message) Ok(code) -> save_location(ctx, p, warehouse_id, code) } } } } fn save_location( ctx: Context, p: Principal, warehouse_id: Int, code: location.LocationCode, ) -> Response { let now = clock.now_local() case database.transaction(ctx.db, fn(conn) { use warehouse <- result.try(repo.get_warehouse(conn, warehouse_id)) case warehouse { None -> Ok(Error("仓库不存在")) Some(row) -> case row.operational { False -> Ok(Error("该仓库只作显示,不能建立库存库位")) True -> { use existing <- result.try(repo.find_location(conn, warehouse_id, code.code)) case existing { Some(_) -> Ok(Error("库位已存在")) None -> { let row = Location( warehouse_id: warehouse_id, area: code.area, aisle: code.aisle, level: code.level, bin: code.bin, code: code.code, barcode: location.barcode(row.warehouse.code, code.compact), enabled: True, created_at: now, updated_at: now, ) use _ <- result.try(repo.insert_location(conn, row)) Ok(Ok(row)) } } } } } }) { Error(database.DbError(message)) -> json_error(500, message) Ok(Error(message)) -> case message { "仓库不存在" -> json_error(404, message) "该仓库只作显示,不能建立库存库位" -> json_error(409, message) _ -> json_error(409, message) } Ok(Ok(row)) -> { access.audit(ctx.db, p, "location.create", row.code, warehouse_id, "") json_ok([#("location", location_json(row))]) } } } fn disable_location(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) case decode.run(body, disable_location_decoder()) { Error(_) -> json_error(400, "库位参数无法解析") Ok(#(warehouse_id, code)) -> { use <- in_scope(p, warehouse_id) let code = string.uppercase(string.trim(code)) case database.transaction(ctx.db, fn(conn) { repo.set_location_enabled(conn, warehouse_id, code, False, clock.now_local()) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(False) -> json_error(404, "库位不存在") Ok(True) -> { access.audit(ctx.db, p, "location.disable", code, warehouse_id, "") json_ok([#("disabled", json.bool(True)), #("code", json.string(code))]) } } } } } fn plan_locations(req: Request, ctx: Context, p: Principal, commit: Bool) -> Response { use body <- wisp.require_json(req) case decode.run(body, plan_decoder()) { Error(_) -> json_error(400, "库位规划参数无法解析") Ok(plan) -> { use <- in_scope(p, plan.warehouse_id) case location_plan.run(ctx.db, plan, commit) { Error(location_plan.PlanRejected(message)) -> json_error(409, message) Error(location_plan.PlanMissingWarehouse) -> json_error(404, "仓库不存在") Error(location_plan.PlanDb(database.DbError(message))) -> json_error(500, message) Ok(outcome) -> { case commit { True -> access.audit( ctx.db, p, "location.plan", string.join(plan.areas, ","), plan.warehouse_id, "新建 " <> int.to_string(list.length(outcome.created)) <> " 个库位", ) False -> Nil } json_ok([ #("committed", json.bool(commit)), #("warehouse_id", json.int(plan.warehouse_id)), #("new_count", json.int(list.length(outcome.created))), #("existing_count", json.int(list.length(outcome.existing))), #("locations", json.array(outcome.created, of: location_json)), #("existing", json.array(outcome.existing, of: json.string)), ]) } } } } } fn plan_decoder() -> decode.Decoder(location_plan.Plan) { let range = { use from <- decode.field("from", json_codec.int()) use to <- decode.field("to", json_codec.int()) use width <- decode.optional_field("width", 2, json_codec.int()) decode.success(location_plan.Range(from: from, to: to, width: width)) } use warehouse_id <- decode.field("warehouse_id", json_codec.int()) use areas <- decode.field("areas", decode.list(decode.string)) use aisle <- decode.field("aisle", range) use level <- decode.field("level", range) use bin <- decode.field("bin", range) decode.success(location_plan.Plan( warehouse_id: warehouse_id, areas: areas, aisle: aisle, level: level, bin: bin, )) } fn location_input_decoder() -> decode.Decoder(#(Int, String, String, String, String)) { use warehouse_id <- decode.field("warehouse_id", json_codec.int()) use area <- decode.field("area", decode.string) use aisle <- decode.field("aisle", decode.string) use level <- decode.field("level", decode.string) use bin <- decode.field("bin", decode.string) decode.success(#(warehouse_id, area, aisle, level, bin)) } fn disable_location_decoder() -> decode.Decoder(#(Int, String)) { use warehouse_id <- decode.field("warehouse_id", json_codec.int()) use code <- decode.field("code", decode.string) decode.success(#(warehouse_id, code)) } fn location_json(row: Location) -> json.Json { json.object([ #("warehouse_id", json.int(row.warehouse_id)), #("area", json.string(row.area)), #("aisle", json.string(row.aisle)), #("level", json.string(row.level)), #("bin", json.string(row.bin)), #("code", json.string(row.code)), #("barcode", json.string(row.barcode)), #("enabled", json.bool(row.enabled)), #("created_at", json.string(row.created_at)), #("updated_at", json.string(row.updated_at)), ]) } fn list_products(req: Request, ctx: Context) -> Response { let keyword = query_value(req, "keyword", "") let limit = query_limit(req) let offset = query_offset(req) case database.transaction(ctx.db, fn(conn) { repo.list_products(conn, keyword, limit, offset) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(page) -> json_ok([ #("total", json.int(page.total)), #("limit", json.int(limit)), #("offset", json.int(offset)), #("products", json.array(page.items, of: product_json)), ]) } } fn list_orders( req: Request, ctx: Context, p: Principal, status: Option(Int), ) -> Response { let limit = query_limit(req) let offset = query_offset(req) case database.transaction(ctx.db, fn(conn) { repo.list_orders(conn, p.scope, status, limit, offset) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(page) -> json_ok([ #("total", json.int(page.total)), #("limit", json.int(limit)), #("offset", json.int(offset)), #("orders", json.array(page.items, of: summary_json)), ]) } } fn list_inbounds( req: Request, ctx: Context, p: Principal, bill_type: Int, status: Option(Int), ) -> Response { let limit = query_limit(req) let offset = query_offset(req) case database.transaction(ctx.db, fn(conn) { repo.list_inbounds(conn, p.scope, bill_type, status, limit, offset) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(page) -> json_ok([ #("total", json.int(page.total)), #("limit", json.int(limit)), #("offset", json.int(offset)), #("inbounds", json.array(page.items, of: inbound_summary_json)), ]) } } fn inbound_detail(ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> purchase_document(ctx, p, bill_id, "本地没有这张采购订单,请先同步") } } fn purchase_receipt_detail(ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> purchase_document(ctx, p, bill_id, "本地没有这张采购入库单,请先同步") } } fn batch_lookup(ctx: Context, p: Principal, batch_code: String) -> Response { case putaway.lookup(ctx.db, batch_code) { Error(putaway.PutawayRejected(message)) -> json_error(404, message) Error(putaway.PutawayDb(database.DbError(message))) -> json_error(500, message) Ok(#(label, put_away, produced_on, expires_on)) -> { use <- in_scope(p, label.warehouse_id) json_ok([ #("batch_code", json.string(label.batch_code)), #("bill_id", json.int(label.bill_id)), #("bill_code", json.string(label.bill_code)), #("bill_detail_id", json.int(label.bill_detail_id)), #("warehouse_id", json.int(label.warehouse_id)), #("warehouse_name", json.string(label.warehouse_name)), #("sku_id", json.int(label.sku_id)), #("goods_code", json.string(label.goods_code)), #("goods_name", json.string(label.goods_name)), #("barcode", json.string(label.barcode)), #("base_qty", json.float(label.base_qty)), #("base_unit", json.string(label.base_unit)), #("put_away_qty", json.float(put_away)), #("produced_on", json.string(produced_on)), #("expires_on", json.string(expires_on)), ]) } } } fn recommend_putaway(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) let decoder = { use batch_code <- decode.field("batch_code", decode.string) use expires_on <- decode.optional_field("expires_on", "", decode.string) decode.success(#(batch_code, expires_on)) } case decode.run(body, decoder) { Error(_) -> json_error(400, "需要 batch_code") Ok(#(batch_code, expires_on)) -> case putaway.suggest(ctx.db, batch_code, expires_on) { Error(err) -> putaway_error(err) Ok(target) -> { use <- at_station(p, target.label.warehouse_id) json_ok([ #("batch_code", json.string(target.label.batch_code)), #("bill_id", json.int(target.label.bill_id)), #("bill_code", json.string(target.label.bill_code)), #("warehouse_id", json.int(target.label.warehouse_id)), #("warehouse_name", json.string(target.label.warehouse_name)), #("goods_code", json.string(target.label.goods_code)), #("goods_name", json.string(target.label.goods_name)), #("barcode", json.string(target.label.barcode)), #("base_qty", json.float(target.label.base_qty)), #("base_unit", json.string(target.label.base_unit)), #("put_away_qty", json.float(target.put_away_qty)), #("produced_on", json.string(target.produced_on)), #("expires_on", json.string(target.expires_on)), #("dates_locked", json.bool(target.produced_on != "")), #("location_code", json.string(target.suggestion.location_code)), #("reason", json.string(target.suggestion.reason)), ]) } } } } fn create_putaway(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) case decode.run(body, putaway_input_decoder()) { Error(_) -> json_error(400, "上架参数无法解析") Ok(input) -> case putaway.lookup(ctx.db, input.batch_code) { Error(err) -> putaway_error(err) Ok(#(label, _, _, _)) -> { use <- at_station(p, label.warehouse_id) case putaway.create(ctx.db, input) { Error(err) -> putaway_error(err) Ok(row) -> { access.audit( ctx.db, p, "putaway.create", row.batch_code, row.warehouse_id, float.to_string(row.qty) <> " 上架到 " <> row.location_code, ) json_ok([#("putaway", putaway_json(row))]) } } } } } } fn putaway_error(err: putaway.PutawayError) -> Response { case err { putaway.PutawayRejected(message) -> json_error(409, message) putaway.PutawayDb(database.DbError(message)) -> json_error(500, message) } } fn putaway_input_decoder() -> decode.Decoder(putaway.PutawayInput) { use batch_code <- decode.field("batch_code", decode.string) use qty <- decode.field("qty", json_codec.number()) use produced_on <- decode.field("produced_on", decode.string) use expires_on <- decode.field("expires_on", decode.string) use location_code <- decode.field("location_code", decode.string) decode.success(putaway.PutawayInput( batch_code: batch_code, qty: qty, produced_on: produced_on, expires_on: expires_on, location_code: location_code, )) } fn putaway_json(row: repo.Putaway) -> json.Json { json.object([ #("id", json.string(row.id)), #("batch_code", json.string(row.batch_code)), #("bill_id", json.int(row.bill_id)), #("bill_detail_id", json.int(row.bill_detail_id)), #("warehouse_id", json.int(row.warehouse_id)), #("sku_id", json.int(row.sku_id)), #("goods_code", json.string(row.goods_code)), #("goods_name", json.string(row.goods_name)), #("location_code", json.string(row.location_code)), #("produced_on", json.string(row.produced_on)), #("expires_on", json.string(row.expires_on)), #("qty", json.float(row.qty)), #("created_at", json.string(row.created_at)), ]) } fn purchase_document( ctx: Context, p: Principal, bill_id: Int, missing: String, ) -> Response { case database.transaction(ctx.db, fn(conn) { use order <- result.try(repo.get_inbound(conn, bill_id)) use codes <- result.try(repo.list_batch_codes(conn, bill_id)) Ok(#(order, codes)) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(#(None, _)) -> json_error(404, missing) Ok(#(Some(order), codes)) -> { use <- in_scope(p, order.warehouse_id) json_ok([#("inbound", inbound_json(order, codes))]) } } } /// 读出采购单所属仓库,用于范围校验。 fn with_inbound_warehouse( ctx: Context, bill_id: Int, next: fn(Int) -> Response, ) -> Response { case database.transaction(ctx.db, fn(conn) { repo.get_inbound(conn, bill_id) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(None) -> json_error(404, "本地没有这张采购单,请先同步") Ok(Some(order)) -> next(order.warehouse_id) } } fn with_order_warehouse( ctx: Context, bill_id: Int, next: fn(Int) -> Response, ) -> Response { case database.transaction(ctx.db, fn(conn) { repo.get_order(conn, bill_id) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(None) -> json_error(404, "本地没有这张订单,请先同步订单") Ok(Some(order)) -> next(order.warehouse_id) } } fn list_receipts(ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> { use warehouse_id <- with_inbound_warehouse(ctx, bill_id) use <- in_scope(p, warehouse_id) case database.transaction(ctx.db, fn(conn) { repo.list_receipts(conn, bill_id) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(rows) -> json_ok([#("receipts", json.array(rows, of: receipt_json))]) } } } } fn create_receipt(req: Request, ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> { use body <- wisp.require_json(req) use warehouse_id <- with_inbound_warehouse(ctx, bill_id) use <- at_station(p, warehouse_id) case decode.run(body, receipt_input_decoder()) { Error(_) -> json_error(400, "收货参数无法解析") Ok(lines) -> case receiving.create(ctx.db, bill_id, lines) { Ok(receipt) -> { access.audit( ctx.db, p, "receipt.create", receipt.id, receipt.warehouse_id, "采购单 " <> int.to_string(bill_id) <> "," <> int.to_string(list.length(receipt.lines)) <> " 行", ) json_ok([#("receipt", receipt_json(receipt))]) } Error(receiving.ReceiptRejected(message)) -> json_error(409, message) Error(receiving.ReceiptDb(database.DbError(message))) -> json_error(500, message) } } } } } fn list_inventory(req: Request, ctx: Context, p: Principal) -> Response { case int.parse(query_value(req, "warehouse_id", "")) { Error(_) -> json_error(400, "需要 warehouse_id") Ok(warehouse_id) -> { use <- in_scope(p, warehouse_id) case database.transaction(ctx.db, fn(conn) { use warehouse <- result.try(repo.get_warehouse(conn, warehouse_id)) use rows <- result.try(repo.list_inventory(conn, warehouse_id)) Ok(#(warehouse, rows)) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(#(None, _)) -> json_error(404, "仓库不存在") Ok(#(Some(warehouse), rows)) -> case warehouse.operational { False -> json_error(409, "该仓库只作显示,没有库存") True -> json_ok([ #("warehouse_id", json.int(warehouse_id)), #("batches", json.array(rows, of: inventory_json)), ]) } } } } } fn receipt_input_decoder() -> decode.Decoder(List(receiving.LineInput)) { use lines <- decode.field( "lines", decode.list({ use bill_detail_id <- decode.field("bill_detail_id", json_codec.int()) use received_qty <- decode.field("received_qty", json_codec.number()) use batch_no <- decode.field("batch_no", decode.string) use produced_on <- decode.field("produced_on", decode.string) use expires_on <- decode.field("expires_on", decode.string) use location_code <- decode.field("location_code", decode.string) decode.success(receiving.LineInput( bill_detail_id: bill_detail_id, received_qty: received_qty, batch_no: batch_no, produced_on: produced_on, expires_on: expires_on, location_code: location_code, )) }), ) decode.success(lines) } fn receipt_json(receipt: repo.Receipt) -> json.Json { json.object([ #("id", json.string(receipt.id)), #("bill_id", json.int(receipt.bill_id)), #("warehouse_id", json.int(receipt.warehouse_id)), #("created_at", json.string(receipt.created_at)), #("lines", json.array(receipt.lines, of: receipt_line_json)), ]) } fn receipt_line_json(line: repo.ReceiptLine) -> json.Json { json.object([ #("line_no", json.int(line.line_no)), #("bill_detail_id", json.int(line.bill_detail_id)), #("sku_id", json.int(line.sku_id)), #("goods_code", json.string(line.goods_code)), #("goods_name", json.string(line.goods_name)), #("barcode", json.string(line.barcode)), #("received_qty", json.float(line.received_qty)), #("batch_no", json.string(line.batch_no)), #("produced_on", json.string(line.produced_on)), #("expires_on", json.string(line.expires_on)), #("location_code", json.string(line.location_code)), ]) } fn inventory_json(batch: repo.InventoryBatch) -> json.Json { json.object([ #("warehouse_id", json.int(batch.warehouse_id)), #("location_code", json.string(batch.location_code)), #("sku_id", json.int(batch.sku_id)), #("goods_code", json.string(batch.goods_code)), #("batch_no", json.string(batch.batch_no)), #("produced_on", json.string(batch.produced_on)), #("expires_on", json.string(batch.expires_on)), #("qty", json.float(batch.qty)), ]) } fn list_shipments(ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> case database.transaction(ctx.db, fn(conn) { use order <- result.try(repo.get_order(conn, bill_id)) use rows <- result.try(repo.list_shipments(conn, bill_id)) Ok(#(order, rows)) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(#(None, _)) -> json_error(404, "本地没有这张订单,请先同步订单") Ok(#(Some(order), rows)) -> { use <- in_scope(p, order.warehouse_id) json_ok([#("shipments", json.array(rows, of: shipment_json))]) } } } } fn create_shipment(req: Request, ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> { use body <- wisp.require_json(req) use warehouse_id <- with_order_warehouse(ctx, bill_id) use <- at_station(p, warehouse_id) case decode.run(body, shipment_input_decoder()) { Error(_) -> json_error(400, "出库参数无法解析") Ok(lines) -> case shipping.create(ctx.db, bill_id, lines) { Ok(shipment) -> { access.audit( ctx.db, p, "shipment.create", shipment.id, shipment.warehouse_id, "销售订单 " <> int.to_string(bill_id) <> "," <> int.to_string(list.length(shipment.lines)) <> " 个批次行", ) json_ok([#("shipment", shipment_json(shipment))]) } Error(shipping.ShipmentRejected(message)) -> json_error(409, message) Error(shipping.ShipmentDb(database.DbError(message))) -> json_error(500, message) } } } } } fn shipment_input_decoder() -> decode.Decoder(List(shipping.ShipLine)) { use lines <- decode.field( "lines", decode.list({ use bill_detail_id <- decode.field("bill_detail_id", json_codec.int()) use qty <- decode.field("qty", json_codec.number()) decode.success(shipping.ShipLine(bill_detail_id: bill_detail_id, qty: qty)) }), ) decode.success(lines) } fn shipment_json(shipment: repo.Shipment) -> json.Json { json.object([ #("id", json.string(shipment.id)), #("bill_id", json.int(shipment.bill_id)), #("warehouse_id", json.int(shipment.warehouse_id)), #("created_at", json.string(shipment.created_at)), #("lines", json.array(shipment.lines, of: shipment_line_json)), ]) } fn shipment_line_json(line: repo.ShipmentLine) -> json.Json { json.object([ #("line_no", json.int(line.line_no)), #("bill_detail_id", json.int(line.bill_detail_id)), #("sku_id", json.int(line.sku_id)), #("goods_code", json.string(line.goods_code)), #("goods_name", json.string(line.goods_name)), #("batch_no", json.string(line.batch_no)), #("produced_on", json.string(line.produced_on)), #("expires_on", json.string(line.expires_on)), #("location_code", json.string(line.location_code)), #("qty", json.float(line.qty)), ]) } fn order_detail(ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> case database.transaction(ctx.db, fn(conn) { repo.get_order(conn, bill_id) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(None) -> json_error(404, "本地没有这张订单,请先同步订单") Ok(Some(order)) -> { use <- in_scope(p, order.warehouse_id) json_ok([#("order", order_json(order))]) } } } } fn picking_task(ctx: Context, p: Principal, bill_id_str: String) -> Response { case int.parse(bill_id_str) { Error(_) -> json_error(400, "单据编号无效") Ok(bill_id) -> case database.transaction(ctx.db, fn(conn) { use order <- result.try(repo.get_order(conn, bill_id)) use shipped_out <- result.try(repo.order_shipped_out(conn, bill_id)) Ok(#(order, shipped_out)) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(#(None, _)) -> json_error(404, "本地没有这张订单,请先同步订单") Ok(#(Some(order), shipped_out)) -> { use <- in_scope(p, order.warehouse_id) case shipped_out, order.items { True, _ -> json_error(409, "这张订单已在本地发完,不再拣货") False, [] -> json_error(409, "订单明细尚未同步") False, _ -> { let task = picking.generate_single_order_picking(order) json_ok([#("task", picking.task_to_json(task))]) } } } } } } fn read_runs(ctx: Context) -> List(repo.SyncRun) { case database.transaction(ctx.db, repo.list_runs) { Ok(runs) -> runs Error(_) -> [] } } fn product_mode(req: Request) -> Result(sync_products.Mode, String) { case string.lowercase(query_value(req, "mode", "incremental")) { "" | "incremental" -> Ok(sync_products.Incremental) "full" -> Ok(sync_products.Full) _ -> Error("mode 只能是 incremental 或 full") } } fn query_value(req: Request, name: String, default: String) -> String { case list.key_find(wisp.get_query(req), name) { Ok(value) -> value Error(_) -> default } } fn query_limit(req: Request) -> Int { case int.parse(query_value(req, "limit", "50")) { Ok(value) if value > 200 -> 200 Ok(value) if value > 0 -> value _ -> 50 } } fn query_offset(req: Request) -> Int { case int.parse(query_value(req, "offset", "0")) { Ok(value) if value >= 0 -> value _ -> 0 } } fn driver_name(ctx: Context) -> String { case ctx.config.db_driver { Sqlite -> "sqlite" Postgres -> "postgres" } } fn report_json(report: sync_report.SyncReport) -> List(#(String, json.Json)) { [ #("success", json.bool(True)), #("entity", json.string(report.entity)), #("trigger", json.string(report.trigger)), #("fetched", json.int(report.fetched)), #("upserted", json.int(report.upserted)), #("cursor", json.string(report.cursor)), ] } fn runs_json(runs: List(repo.SyncRun)) -> json.Json { json.array(runs, of: fn(run) { json.object([ #("id", json.string(run.id)), #("entity", json.string(run.entity)), #("trigger", json.string(run.trigger)), #("started_at", json.string(run.started_at)), #("finished_at", json.string(run.finished_at)), #("status", json.string(run.status)), #("fetched", json.int(run.fetched)), #("upserted", json.int(run.upserted)), #("error_message", json.string(run.error_message)), ]) }) } fn product_json(item: GoodsItem) -> json.Json { json.object([ #("id", json.int(item.id)), #("code", json.string(item.code)), #("name", json.string(item.name)), #("barcode", json.string(item.barcode)), #("spec", json.string(item.spec)), #("unit", json.string(item.unit)), #("shelf_life", json.string(item.shelf_life)), #("shelf_life_days", json.int(item.shelf_life_days)), #("position_code", json.string(item.position_code)), #("disabled", json.bool(item.disabled)), #("modified_at", json.string(item.modified_at)), #("created_at", json.string(item.created_at)), ]) } fn summary_json(order: SalesOrderSummary) -> json.Json { json.object([ #("bill_id", json.int(order.bill_id)), #("bill_code", json.string(order.bill_code)), #("customer_name", json.string(order.customer_name)), #("contact_person", json.string(order.contact_person)), #("phone", json.string(order.phone)), #("warehouse_name", json.string(order.warehouse_name)), #("warehouse_id", json.int(order.warehouse_id)), #("bill_date", json.string(order.bill_date)), #("status", json.int(order.status)), #("status_name", json.string(order.status_name)), #("total_qty", json.float(order.total_qty)), #("total_money", json.float(order.total_money)), #("remark", json.string(order.remark)), ]) } fn order_json(order: SalesOrder) -> json.Json { json.object([ #("bill_id", json.int(order.bill_id)), #("bill_code", json.string(order.bill_code)), #("customer_name", json.string(order.customer_name)), #("contact_person", json.string(order.contact_person)), #("phone", json.string(order.phone)), #("warehouse_name", json.string(order.warehouse_name)), #("warehouse_id", json.int(order.warehouse_id)), #("bill_date", json.string(order.bill_date)), #("status", json.int(order.status)), #("status_name", json.string(order.status_name)), #("total_qty", json.float(order.total_qty)), #("total_money", json.float(order.total_money)), #("remark", json.string(order.remark)), #("items", json.array(order.items, of: item_json)), ]) } fn inbound_summary_json(order: PurchaseOrderSummary) -> json.Json { json.object([ #("bill_id", json.int(order.bill_id)), #("bill_code", json.string(order.bill_code)), #("supplier_name", json.string(order.supplier_name)), #("contact_person", json.string(order.contact_person)), #("phone", json.string(order.phone)), #("warehouse_name", json.string(order.warehouse_name)), #("warehouse_id", json.int(order.warehouse_id)), #("bill_date", json.string(order.bill_date)), #("status", json.int(order.status)), #("status_name", json.string(order.status_name)), #("total_qty", json.float(order.total_qty)), #("total_money", json.float(order.total_money)), #("remark", json.string(order.remark)), ]) } fn inbound_json( order: PurchaseOrder, codes: List(#(Int, String)), ) -> json.Json { let codes = dict.from_list(codes) json.object([ #("bill_id", json.int(order.bill_id)), #("bill_code", json.string(order.bill_code)), #("supplier_name", json.string(order.supplier_name)), #("contact_person", json.string(order.contact_person)), #("phone", json.string(order.phone)), #("warehouse_name", json.string(order.warehouse_name)), #("warehouse_id", json.int(order.warehouse_id)), #("bill_date", json.string(order.bill_date)), #("status", json.int(order.status)), #("status_name", json.string(order.status_name)), #("total_qty", json.float(order.total_qty)), #("total_money", json.float(order.total_money)), #("remark", json.string(order.remark)), #("items", json.array(order.items, of: inbound_item_json(codes))), ]) } fn inbound_item_json(codes: dict.Dict(Int, String)) -> fn(PurchaseOrderItem) -> json.Json { fn(item: PurchaseOrderItem) { let code = case dict.get(codes, item.bill_detail_id) { Ok(code) -> code Error(_) -> "" } json.object([ #("bill_detail_id", json.int(item.bill_detail_id)), #("batch_code", json.string(code)), #("sku_id", json.int(item.sku_id)), #("goods_code", json.string(item.goods_code)), #("goods_name", json.string(item.goods_name)), #("barcode", json.string(item.barcode)), #("spec", json.string(item.spec)), #("package_qty", json.float(item.package_qty)), #("package_unit", json.string(item.package_unit)), #("base_qty", json.float(item.base_qty)), #("base_unit", json.string(item.base_unit)), #("price", json.float(item.price)), #("total_money", json.float(item.total_money)), #("position_code", json.string(item.position_code)), #("area_code", json.string(item.area_code)), ]) } } fn item_json(item: SalesOrderItem) -> json.Json { json.object([ #("bill_detail_id", json.int(item.bill_detail_id)), #("sku_id", json.int(item.sku_id)), #("goods_code", json.string(item.goods_code)), #("goods_name", json.string(item.goods_name)), #("barcode", json.string(item.barcode)), #("spec", json.string(item.spec)), #("package_qty", json.float(item.package_qty)), #("package_unit", json.string(item.package_unit)), #("base_qty", json.float(item.base_qty)), #("base_unit", json.string(item.base_unit)), #("price", json.float(item.price)), #("total_money", json.float(item.total_money)), #("position_code", json.string(item.position_code)), #("area_code", json.string(item.area_code)), ]) } fn wave_candidates(req: Request, ctx: Context, p: Principal) -> Response { case station_query(req, p) { Error(response) -> response Ok(warehouse_id) -> { let limit = query_limit(req) let offset = query_offset(req) case database.transaction(ctx.db, fn(conn) { repo.list_wave_candidates(conn, p.scope, warehouse_id, limit, offset) }) { Error(database.DbError(message)) -> json_error(500, message) Ok(page) -> json_ok([ #("total", json.int(page.total)), #("limit", json.int(limit)), #("offset", json.int(offset)), #("orders", json.array(page.items, of: summary_json)), ]) } } } } fn list_waves(req: Request, ctx: Context, p: Principal) -> Response { case station_query(req, p) { Error(response) -> response Ok(warehouse_id) -> { let limit = query_limit(req) let offset = query_offset(req) case wave.list(ctx.db, warehouse_id, limit, offset) { Error(err) -> wave_error(err) Ok(#(total, rows)) -> json_ok([ #("total", json.int(total)), #("limit", json.int(limit)), #("offset", json.int(offset)), #("waves", json.array(rows, of: wave_summary_json)), ]) } } } } fn create_wave(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) case decode.run(body, decode.at(["bill_ids"], decode.list(json_codec.int()))) { Error(_) -> json_error(400, "需要 bill_ids") Ok(bill_ids) -> case p.current_warehouse { 0 -> json_error(409, "请先选择当前作业仓库") warehouse_id -> { use <- in_scope(p, warehouse_id) case wave.create(ctx.db, bill_ids, p.username, warehouse_id) { Error(err) -> wave_error(err) Ok(row) -> { access.audit( ctx.db, p, "wave.create", row.id, row.warehouse_id, int.to_string(list.length(row.orders)) <> " 张订单", ) json_ok([#("wave", wave_json(row))]) } } } } } } fn wave_detail(ctx: Context, p: Principal, wave_id: String) -> Response { case wave.get(ctx.db, wave_id) { Error(err) -> wave_error(err) Ok(row) -> { use <- in_scope(p, row.warehouse_id) json_ok([#("wave", wave_json(row))]) } } } fn cancel_wave(ctx: Context, p: Principal, wave_id: String) -> Response { case p.current_warehouse { 0 -> json_error(409, "请先选择当前作业仓库") warehouse_id -> { use <- in_scope(p, warehouse_id) case wave.cancel(ctx.db, wave_id, warehouse_id) { Error(err) -> wave_error(err) Ok(row) -> { access.audit(ctx.db, p, "wave.cancel", row.id, row.warehouse_id, "") json_ok([#("wave", wave_json(row))]) } } } } } fn station_query(req: Request, p: Principal) -> Result(Int, Response) { case int.parse(query_value(req, "warehouse_id", "0")) { Ok(warehouse_id) if warehouse_id > 0 -> case rbac.allows(p.scope, warehouse_id) { True -> Ok(warehouse_id) False -> Error(json_error(403, "无权访问该仓库")) } _ -> Error(json_error(409, "请先选择当前作业仓库")) } } fn wave_error(err: wave.WaveError) -> Response { case err { wave.WaveRejected("波次不存在") -> json_error(404, "波次不存在") wave.WaveRejected(message) -> json_error(409, message) wave.WaveDb(database.DbError(message)) -> json_error(500, message) } } fn wave_summary_json(row: wave.WaveSummary) -> json.Json { json.object([ #("id", json.string(row.id)), #("warehouse_id", json.int(row.warehouse_id)), #("warehouse_name", json.string(row.warehouse_name)), #("status", json.string(row.status)), #("status_name", json.string(wave_status_name(row.status))), #("order_count", json.int(row.order_count)), #("pick_lines", json.int(row.pick_lines)), #("short_qty", json.float(row.short_qty)), #("created_by", json.string(row.created_by)), #("created_at", json.string(row.created_at)), ]) } fn wave_json(row: wave.Wave) -> json.Json { json.object([ #("id", json.string(row.id)), #("warehouse_id", json.int(row.warehouse_id)), #("warehouse_name", json.string(row.warehouse_name)), #("status", json.string(row.status)), #("status_name", json.string(wave_status_name(row.status))), #("created_by", json.string(row.created_by)), #("created_at", json.string(row.created_at)), #("orders", json.array(row.orders, of: wave_order_json)), #("picks", json.array(row.picks, of: wave_pick_json)), ]) } fn wave_order_json(order: wave.WaveOrder) -> json.Json { json.object([ #("bill_id", json.int(order.bill_id)), #("bill_code", json.string(order.bill_code)), #("customer_name", json.string(order.customer_name)), ]) } fn wave_pick_json(line: wave.PickLine) -> json.Json { json.object([ #("line_no", json.int(line.line_no)), #("sku_id", json.int(line.sku_id)), #("goods_code", json.string(line.goods_code)), #("goods_name", json.string(line.goods_name)), #("barcode", json.string(line.barcode)), #("spec", json.string(line.spec)), #("base_unit", json.string(line.base_unit)), #("location_code", json.string(line.location_code)), #("batch_no", json.string(line.batch_no)), #("produced_on", json.string(line.produced_on)), #("expires_on", json.string(line.expires_on)), #("qty", json.float(line.qty)), #("short_qty", json.float(line.short_qty)), #( "allocations", json.array(line.allocations, of: fn(allocation: wave.Allocation) { json.object([ #("bill_id", json.int(allocation.bill_id)), #("bill_code", json.string(allocation.bill_code)), #("bill_detail_id", json.int(allocation.bill_detail_id)), #("qty", json.float(allocation.qty)), ]) }), ), ]) } fn wave_status_name(status: String) -> String { case status { "open" -> "待拣货" "cancelled" -> "已撤销" _ -> status } } fn list_moves(req: Request, ctx: Context, p: Principal) -> Response { case station_query(req, p) { Error(response) -> response Ok(warehouse_id) -> { let limit = query_limit(req) let offset = query_offset(req) case stock_move.list(ctx.db, warehouse_id, limit, offset) { Error(err) -> move_error(err) Ok(#(total, rows)) -> json_ok([ #("total", json.int(total)), #("limit", json.int(limit)), #("offset", json.int(offset)), #("moves", json.array(rows, of: move_json)), ]) } } } } fn create_move(req: Request, ctx: Context, p: Principal) -> Response { use body <- wisp.require_json(req) case decode.run(body, move_input_decoder()) { Error(_) -> json_error(400, "移库参数无法解析") Ok(input) -> case p.current_warehouse { 0 -> json_error(409, "请先选择当前作业仓库") warehouse_id -> { use <- in_scope(p, warehouse_id) case stock_move.create(ctx.db, warehouse_id, input, p.username) { Error(err) -> move_error(err) Ok(row) -> { access.audit( ctx.db, p, "move.create", row.id, row.warehouse_id, row.from_location <> " -> " <> row.to_location, ) json_ok([#("move", move_json(row))]) } } } } } } fn move_input_decoder() -> decode.Decoder(stock_move.MoveInput) { use from_location <- decode.field("from_location", decode.string) use sku_id <- decode.field("sku_id", json_codec.int()) use batch_no <- decode.field("batch_no", decode.string) use produced_on <- decode.field("produced_on", decode.string) use expires_on <- decode.field("expires_on", decode.string) use qty <- decode.field("qty", json_codec.number()) use to_location <- decode.field("to_location", decode.string) decode.success(stock_move.MoveInput( from_location: from_location, sku_id: sku_id, batch_no: batch_no, produced_on: produced_on, expires_on: expires_on, qty: qty, to_location: to_location, )) } fn move_json(row: stock_move.Move) -> json.Json { json.object([ #("id", json.string(row.id)), #("warehouse_id", json.int(row.warehouse_id)), #("from_location", json.string(row.from_location)), #("to_location", json.string(row.to_location)), #("sku_id", json.int(row.sku_id)), #("goods_code", json.string(row.goods_code)), #("batch_no", json.string(row.batch_no)), #("produced_on", json.string(row.produced_on)), #("expires_on", json.string(row.expires_on)), #("qty", json.float(row.qty)), #("created_by", json.string(row.created_by)), #("created_at", json.string(row.created_at)), ]) } fn move_error(err: stock_move.MoveError) -> Response { case err { stock_move.MoveRejected("仓库不存在") -> json_error(404, "仓库不存在") stock_move.MoveRejected(message) -> json_error(409, message) stock_move.MoveDb(database.DbError(message)) -> json_error(500, message) } } fn json_ok(fields: List(#(String, json.Json))) -> Response { wisp.json_response(json.to_string(json.object(fields)), 200) } fn json_error(status: Int, message: String) -> Response { wisp.json_response( json.to_string( json.object([ #("success", json.bool(False)), #("error", json.string(message)), ]), ), status, ) }