297 lines
8.4 KiB
Go
297 lines
8.4 KiB
Go
|
|
package handlers
|
||
|
|
|
||
|
|
import (
|
||
|
|
"database/sql"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type InventoryHandler struct {
|
||
|
|
DB *sql.DB
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewInventoryHandler(db *sql.DB) *InventoryHandler {
|
||
|
|
return &InventoryHandler{DB: db}
|
||
|
|
}
|
||
|
|
|
||
|
|
type Warehouse struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
Location string `json:"location"`
|
||
|
|
Address map[string]interface{} `json:"address"`
|
||
|
|
IsDefault bool `json:"is_default"`
|
||
|
|
CreatedAt time.Time `json:"created_at"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type InventoryItem struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
ProductID string `json:"product_id"`
|
||
|
|
ProductName string `json:"product_name"`
|
||
|
|
WarehouseID string `json:"warehouse_id"`
|
||
|
|
WarehouseName string `json:"warehouse_name"`
|
||
|
|
Quantity float64 `json:"quantity"`
|
||
|
|
ReservedQty float64 `json:"reserved_qty"`
|
||
|
|
AvailableQty float64 `json:"available_qty"`
|
||
|
|
ReorderPoint float64 `json:"reorder_point"`
|
||
|
|
ReorderQty float64 `json:"reorder_qty"`
|
||
|
|
UnitCost float64 `json:"unit_cost"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type InventoryMovement struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
ProductID string `json:"product_id"`
|
||
|
|
ProductName string `json:"product_name"`
|
||
|
|
WarehouseID string `json:"warehouse_id"`
|
||
|
|
Type string `json:"type"`
|
||
|
|
Quantity float64 `json:"quantity"`
|
||
|
|
ReferenceType string `json:"reference_type"`
|
||
|
|
ReferenceID string `json:"reference_id"`
|
||
|
|
Notes string `json:"notes"`
|
||
|
|
CreatedAt time.Time `json:"created_at"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *InventoryHandler) ListWarehouses(w http.ResponseWriter, r *http.Request) {
|
||
|
|
rows, err := h.DB.Query(`
|
||
|
|
SELECT id, name, location, address, is_default, created_at
|
||
|
|
FROM boc_warehouses ORDER BY name
|
||
|
|
`)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
warehouses := []Warehouse{}
|
||
|
|
for rows.Next() {
|
||
|
|
var w Warehouse
|
||
|
|
var addr []byte
|
||
|
|
if err := rows.Scan(&w.ID, &w.Name, &w.Location, &addr, &w.IsDefault, &w.CreatedAt); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
json.Unmarshal(addr, &w.Address)
|
||
|
|
warehouses = append(warehouses, w)
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"warehouses": warehouses,
|
||
|
|
"total": len(warehouses),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *InventoryHandler) CreateWarehouse(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req Warehouse
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
|
writeError(w, http.StatusBadRequest, "invalid request")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
addr, _ := json.Marshal(req.Address)
|
||
|
|
|
||
|
|
var id string
|
||
|
|
err := h.DB.QueryRow(`
|
||
|
|
INSERT INTO boc_warehouses (name, location, address, is_default)
|
||
|
|
VALUES ($1, $2, $3, $4)
|
||
|
|
RETURNING id
|
||
|
|
`, req.Name, req.Location, addr, req.IsDefault).Scan(&id)
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "failed to create warehouse")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||
|
|
"id": id,
|
||
|
|
"message": "Warehouse created",
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *InventoryHandler) ListInventory(w http.ResponseWriter, r *http.Request) {
|
||
|
|
warehouseID := r.URL.Query().Get("warehouse_id")
|
||
|
|
|
||
|
|
var query string
|
||
|
|
var args []interface{}
|
||
|
|
if warehouseID != "" {
|
||
|
|
query = `
|
||
|
|
SELECT i.id, i.product_id, p.name, i.warehouse_id, w.name, i.quantity, i.reserved_qty, i.reorder_point, i.reorder_qty, i.unit_cost
|
||
|
|
FROM boc_inventory i
|
||
|
|
JOIN boc_products p ON i.product_id = p.id
|
||
|
|
JOIN boc_warehouses w ON i.warehouse_id = w.id
|
||
|
|
WHERE i.warehouse_id = $1
|
||
|
|
ORDER BY p.name
|
||
|
|
`
|
||
|
|
args = append(args, warehouseID)
|
||
|
|
} else {
|
||
|
|
query = `
|
||
|
|
SELECT i.id, i.product_id, p.name, i.warehouse_id, w.name, i.quantity, i.reserved_qty, i.reorder_point, i.reorder_qty, i.unit_cost
|
||
|
|
FROM boc_inventory i
|
||
|
|
JOIN boc_products p ON i.product_id = p.id
|
||
|
|
JOIN boc_warehouses w ON i.warehouse_id = w.id
|
||
|
|
ORDER BY p.name
|
||
|
|
`
|
||
|
|
}
|
||
|
|
|
||
|
|
rows, err := h.DB.Query(query, args...)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
items := []InventoryItem{}
|
||
|
|
for rows.Next() {
|
||
|
|
var i InventoryItem
|
||
|
|
if err := rows.Scan(&i.ID, &i.ProductID, &i.ProductName, &i.WarehouseID, &i.WarehouseName, &i.Quantity, &i.ReservedQty, &i.ReorderPoint, &i.ReorderQty, &i.UnitCost); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
i.AvailableQty = i.Quantity - i.ReservedQty
|
||
|
|
items = append(items, i)
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"inventory": items,
|
||
|
|
"total": len(items),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *InventoryHandler) AdjustStock(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req struct {
|
||
|
|
ProductID string `json:"product_id"`
|
||
|
|
WarehouseID string `json:"warehouse_id"`
|
||
|
|
Quantity float64 `json:"quantity"`
|
||
|
|
Reason string `json:"reason"`
|
||
|
|
}
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
|
writeError(w, http.StatusBadRequest, "invalid request")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
tx, err := h.DB.Begin()
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "transaction error")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer tx.Rollback()
|
||
|
|
|
||
|
|
// Update or insert inventory
|
||
|
|
var existingID string
|
||
|
|
err = tx.QueryRow(`
|
||
|
|
SELECT id FROM boc_inventory WHERE product_id = $1 AND warehouse_id = $2
|
||
|
|
`, req.ProductID, req.WarehouseID).Scan(&existingID)
|
||
|
|
|
||
|
|
if err == sql.ErrNoRows {
|
||
|
|
// Insert new
|
||
|
|
_, err = tx.Exec(`
|
||
|
|
INSERT INTO boc_inventory (product_id, warehouse_id, quantity)
|
||
|
|
VALUES ($1, $2, $3)
|
||
|
|
`, req.ProductID, req.WarehouseID, req.Quantity)
|
||
|
|
} else if err == nil {
|
||
|
|
// Update existing
|
||
|
|
_, err = tx.Exec(`
|
||
|
|
UPDATE boc_inventory SET quantity = $1, updated_at = NOW() WHERE id = $2
|
||
|
|
`, req.Quantity, existingID)
|
||
|
|
}
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "failed to update inventory")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record movement
|
||
|
|
_, err = tx.Exec(`
|
||
|
|
INSERT INTO boc_inventory_movements (product_id, warehouse_id, type, quantity, notes)
|
||
|
|
VALUES ($1, $2, 'adjustment', $3, $4)
|
||
|
|
`, req.ProductID, req.WarehouseID, req.Quantity, req.Reason)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "failed to record movement")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := tx.Commit(); err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "commit failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"message": "Stock adjusted",
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *InventoryHandler) ListMovements(w http.ResponseWriter, r *http.Request) {
|
||
|
|
productID := r.URL.Query().Get("product_id")
|
||
|
|
|
||
|
|
var query string
|
||
|
|
var args []interface{}
|
||
|
|
if productID != "" {
|
||
|
|
query = `
|
||
|
|
SELECT m.id, m.product_id, p.name, m.warehouse_id, m.type, m.quantity, m.reference_type, m.reference_id, m.notes, m.created_at
|
||
|
|
FROM boc_inventory_movements m
|
||
|
|
JOIN boc_products p ON m.product_id = p.id
|
||
|
|
WHERE m.product_id = $1
|
||
|
|
ORDER BY m.created_at DESC
|
||
|
|
LIMIT 100
|
||
|
|
`
|
||
|
|
args = append(args, productID)
|
||
|
|
} else {
|
||
|
|
query = `
|
||
|
|
SELECT m.id, m.product_id, p.name, m.warehouse_id, m.type, m.quantity, m.reference_type, m.reference_id, m.notes, m.created_at
|
||
|
|
FROM boc_inventory_movements m
|
||
|
|
JOIN boc_products p ON m.product_id = p.id
|
||
|
|
ORDER BY m.created_at DESC
|
||
|
|
LIMIT 100
|
||
|
|
`
|
||
|
|
}
|
||
|
|
|
||
|
|
rows, err := h.DB.Query(query, args...)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
movements := []InventoryMovement{}
|
||
|
|
for rows.Next() {
|
||
|
|
var m InventoryMovement
|
||
|
|
if err := rows.Scan(&m.ID, &m.ProductID, &m.ProductName, &m.WarehouseID, &m.Type, &m.Quantity, &m.ReferenceType, &m.ReferenceID, &m.Notes, &m.CreatedAt); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
movements = append(movements, m)
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"movements": movements,
|
||
|
|
"total": len(movements),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *InventoryHandler) GetLowStock(w http.ResponseWriter, r *http.Request) {
|
||
|
|
rows, err := h.DB.Query(`
|
||
|
|
SELECT i.id, i.product_id, p.name, i.warehouse_id, w.name, i.quantity, i.reserved_qty, i.reorder_point, i.reorder_qty
|
||
|
|
FROM boc_inventory i
|
||
|
|
JOIN boc_products p ON i.product_id = p.id
|
||
|
|
JOIN boc_warehouses w ON i.warehouse_id = w.id
|
||
|
|
WHERE i.quantity <= i.reorder_point
|
||
|
|
ORDER BY (i.quantity / NULLIF(i.reorder_point, 0))
|
||
|
|
`)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
items := []InventoryItem{}
|
||
|
|
for rows.Next() {
|
||
|
|
var i InventoryItem
|
||
|
|
if err := rows.Scan(&i.ID, &i.ProductID, &i.ProductName, &i.WarehouseID, &i.WarehouseName, &i.Quantity, &i.ReservedQty, &i.ReorderPoint, &i.ReorderQty); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
i.AvailableQty = i.Quantity - i.ReservedQty
|
||
|
|
items = append(items, i)
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"low_stock": items,
|
||
|
|
"total": len(items),
|
||
|
|
})
|
||
|
|
}
|