feat: add stock movement on backend
This commit is contained in:
@@ -26,5 +26,6 @@ func InitDB() {
|
|||||||
&models.CartItem{},
|
&models.CartItem{},
|
||||||
&models.Order{},
|
&models.Order{},
|
||||||
&models.OrderItem{},
|
&models.OrderItem{},
|
||||||
|
&models.StockMovement{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"fishfish/database"
|
||||||
|
"fishfish/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StockIn(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
ItemID uint `json:"item_id" binding:"required"`
|
||||||
|
Quantity float64 `json:"quantity" binding:"required"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Неверные данные"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Quantity <= 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Количество должно быть положительным"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := database.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
result := tx.Model(&models.Item{}).
|
||||||
|
Where("id = ? ", input.ItemID).
|
||||||
|
Update("stock", gorm.Expr("stock + ?", input.Quantity))
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return fmt.Errorf("товар не найден")
|
||||||
|
}
|
||||||
|
|
||||||
|
movement := models.StockMovement{
|
||||||
|
ItemID: input.ItemID,
|
||||||
|
Type: "in",
|
||||||
|
Quantity: input.Quantity,
|
||||||
|
Reason: "purchase",
|
||||||
|
Comment: input.Comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&movement).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Приход оформлен"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func StockOut(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
ItemID uint `json:"item_id" binding:"required"`
|
||||||
|
Quantity float64 `json:"quantity" binding:"required"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Неверные данные"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Quantity <= 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Количество должно быть положительным"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := database.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
result := tx.Model(&models.Item{}).
|
||||||
|
Where("id = ? AND stock >= ? ", input.ItemID, input.Quantity).
|
||||||
|
Update("stock", gorm.Expr("stock - ?", input.Quantity))
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return fmt.Errorf("товар не найден или не достаточно товара")
|
||||||
|
}
|
||||||
|
|
||||||
|
movement := models.StockMovement{
|
||||||
|
ItemID: input.ItemID,
|
||||||
|
Type: "out",
|
||||||
|
Quantity: input.Quantity,
|
||||||
|
Reason: "sale",
|
||||||
|
Comment: input.Comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&movement).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Уход оформлен"})
|
||||||
|
}
|
||||||
@@ -67,6 +67,8 @@ func main() {
|
|||||||
admin.GET("/orders", handlers.GetAllOrders)
|
admin.GET("/orders", handlers.GetAllOrders)
|
||||||
admin.PATCH("/orders/:id", handlers.UpdateOrderStatus)
|
admin.PATCH("/orders/:id", handlers.UpdateOrderStatus)
|
||||||
admin.PATCH("/items/:id/avatar", handlers.UploadItemAvatar)
|
admin.PATCH("/items/:id/avatar", handlers.UploadItemAvatar)
|
||||||
|
admin.POST("/stock/in", handlers.StockIn)
|
||||||
|
admin.POST("/stock/out", handlers.StockOut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StockMovement struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
ItemID uint `json:"item_id"`
|
||||||
|
Item Item `json:"item" gorm:"foreignKey:ItemID"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Reason string `json:"reason" gorm:"not null"`
|
||||||
|
OrderID *uint `json:"order_id"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
@@ -64,3 +64,5 @@ export const updateOrderStatus = (id, data) =>
|
|||||||
axios.patch(`${API_URL}/admin/orders/${id}`, data);
|
axios.patch(`${API_URL}/admin/orders/${id}`, data);
|
||||||
export const registerAdmin = (data) =>
|
export const registerAdmin = (data) =>
|
||||||
axios.post(`${API_URL}/admin/register`, data);
|
axios.post(`${API_URL}/admin/register`, data);
|
||||||
|
|
||||||
|
export const stockIn = (data) => axios.post(`${API_URL}/admin/stock/in`, data)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function ItemPage() {
|
|||||||
<main className="px-6 md:px-16 grow w-full mb-10 text-white">
|
<main className="px-6 md:px-16 grow w-full mb-10 text-white">
|
||||||
<div className="flex flex-col md:flex-row gap-8 md:gap-12 mt-6">
|
<div className="flex flex-col md:flex-row gap-8 md:gap-12 mt-6">
|
||||||
{/* ЛЕВО — фото */}
|
{/* ЛЕВО — фото */}
|
||||||
<div className="md:w-1/2">
|
<div className="md:w-1/4">
|
||||||
<img
|
<img
|
||||||
src={item.avatar_url}
|
src={item.avatar_url}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
@@ -73,7 +73,7 @@ export default function ItemPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ПРАВО — всё инфо в одной колонке */}
|
{/* ПРАВО — всё инфо в одной колонке */}
|
||||||
<div className="md:w-1/2 flex flex-col">
|
<div className="md:w-3/4 flex flex-col">
|
||||||
<h1 className="text-2xl md:text-4xl font-medium uppercase mb-3">
|
<h1 className="text-2xl md:text-4xl font-medium uppercase mb-3">
|
||||||
{item.name}
|
{item.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user