diff --git a/cmd/main.go b/cmd/main.go index 62821ad..a97e9a0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,6 +13,7 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/modules/auth" "github.com/Caknoooo/go-gin-clean-starter/modules/category" "github.com/Caknoooo/go-gin-clean-starter/modules/client" + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt" maintenancegroup "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group" "github.com/Caknoooo/go-gin-clean-starter/modules/menu" "github.com/Caknoooo/go-gin-clean-starter/modules/monitoring" @@ -161,6 +162,7 @@ func main() { warehouse.RegisterRoutes(server, injector) zona.RegisterRoutes(server, injector) aisle.RegisterRoutes(server, injector) + inventory_receipt.RegisterRoutes(server, injector) // register swagger route server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/database/entities/t_inventory_receipt_entity.go b/database/entities/t_inventory_receipt_entity.go index 11d00a1..aefe764 100644 --- a/database/entities/t_inventory_receipt_entity.go +++ b/database/entities/t_inventory_receipt_entity.go @@ -19,7 +19,8 @@ type TInventoryReceiptEntity struct { ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"` - Client M_Client `gorm:"foreignKey:ClientID;references:ID"` + ReceiptLines []TInventoryReceiptLineEntity `gorm:"foreignKey:InvReceiptID;references:ID"` + Client M_Client `gorm:"foreignKey:ClientID;references:ID"` FullAuditTrail } @@ -29,14 +30,30 @@ func (TInventoryReceiptEntity) TableName() string { } // GenerateDocumentNumber generates a new document number for a client -func GenerateDocumentNumber(db *gorm.DB, clientName string) (string, error) { +func GenerateDocumentNumber(db *gorm.DB, clientId string) (string, error) { prefix := "RCPT" - firstLetter := strings.ToUpper(string(clientName[0])) + // Ambil nama client berdasarkan clientId + var client struct { + Name string + } + if err := db.Table("m_clients").Select("name").Where("id = ?", clientId).First(&client).Error; err != nil { + return "", fmt.Errorf("client not found") + } + if client.Name == "" { + return "", fmt.Errorf("client name is empty") + } + words := strings.Fields(client.Name) + initials := "" + for _, w := range words { + if len(w) > 0 { + initials += strings.ToUpper(string(w[0])) + } + } + // Cari document number terakhir untuk client ini var lastReceipt TInventoryReceiptEntity err := db. - Joins("JOIN m_clients ON m_clients.id = t_inventory_receipts.client_id"). - Where("m_clients.name = ?", clientName). + Where("client_id = ?", clientId). Order("document_number DESC"). First(&lastReceipt).Error @@ -49,6 +66,6 @@ func GenerateDocumentNumber(db *gorm.DB, clientName string) (string, error) { } } - docNum := fmt.Sprintf("%s-%s-%04d", prefix, firstLetter, seq) + docNum := fmt.Sprintf("%s-%s-%04d", prefix, initials, seq) return docNum, nil } diff --git a/database/entities/t_inventory_receipt_line_entity.go b/database/entities/t_inventory_receipt_line_entity.go index 1d31fbc..620b3b9 100644 --- a/database/entities/t_inventory_receipt_line_entity.go +++ b/database/entities/t_inventory_receipt_line_entity.go @@ -10,10 +10,10 @@ type TInventoryReceiptLineEntity struct { BatchNumber string `gorm:"type:varchar(100);" json:"batch_number"` RepackingSuggestion string `gorm:"type:text;" json:"repacking_suggestion"` - RepackUomID uuid.UUID `gorm:"type:uuid;index;" json:"repack_uom_id"` - InvReceiptID uuid.UUID `gorm:"type:uuid;index;" json:"inv_receipt_id"` - ProductID uuid.UUID `gorm:"type:uuid;index;" json:"product_id"` - ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"` + RepackUomID *uuid.UUID `gorm:"type:uuid;index;" json:"repack_uom_id"` + InvReceiptID uuid.UUID `gorm:"type:uuid;index;" json:"inv_receipt_id"` + ProductID uuid.UUID `gorm:"type:uuid;index;" json:"product_id"` + ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"` Product MProductEntity `gorm:"foreignKey:ProductID;references:ID"` RepackUom MUomEntity `gorm:"foreignKey:RepackUomID;references:ID"` diff --git a/modules/inventory_receipt/controller/inventory_receipt_controller.go b/modules/inventory_receipt/controller/inventory_receipt_controller.go new file mode 100644 index 0000000..b8d12c1 --- /dev/null +++ b/modules/inventory_receipt/controller/inventory_receipt_controller.go @@ -0,0 +1,182 @@ +package controller + +import ( + "net/http" + + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/service" + "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" + "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" + "github.com/gin-gonic/gin" + "github.com/samber/do" + "gorm.io/gorm" +) + +type InventoryReceiptController interface { + Create(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) + GetById(ctx *gin.Context) + GetAll(ctx *gin.Context) + CreateLine(ctx *gin.Context) + UpdateLine(ctx *gin.Context) + DeleteLine(ctx *gin.Context) +} + +type inventoryReceiptController struct { + receiptService service.InventoryReceiptService + db *gorm.DB +} + +// DeleteLine implements InventoryReceiptController. +func (c *inventoryReceiptController) DeleteLine(ctx *gin.Context) { + id := ctx.Param("id") + if err := c.receiptService.DeleteLine(ctx, id); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT_LINE, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT_LINE, nil) + ctx.JSON(http.StatusOK, res) +} + +// UpdateLine implements InventoryReceiptController. +func (c *inventoryReceiptController) UpdateLine(ctx *gin.Context) { + id := ctx.Param("id") + var req dto.InventoryReceiptLineUpdateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + updated, err := c.receiptService.UpdateLine(ctx, id, req) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT_LINE, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT_LINE, updated) + ctx.JSON(http.StatusOK, res) +} + +// CreateLine implements InventoryReceiptController. +func (c *inventoryReceiptController) CreateLine(ctx *gin.Context) { + id := ctx.Param("id") + var req dto.InventoryReceiptLineCreateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + created, err := c.receiptService.CreateLine(ctx, id, req) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT_LINE, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT_LINE, created) + ctx.JSON(http.StatusOK, res) +} + +func (c *inventoryReceiptController) Create(ctx *gin.Context) { + var req dto.InventoryReceiptCreateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + created, err := c.receiptService.Create(ctx, req) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT, created) + ctx.JSON(http.StatusOK, res) +} + +func (c *inventoryReceiptController) Update(ctx *gin.Context) { + id := ctx.Param("id") + var req dto.InventoryReceiptUpdateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + updated, err := c.receiptService.Update(ctx, req, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT, updated) + ctx.JSON(http.StatusOK, res) +} + +func (c *inventoryReceiptController) Delete(ctx *gin.Context) { + id := ctx.Param("id") + if err := c.receiptService.Delete(ctx, id); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT, nil) + ctx.JSON(http.StatusOK, res) +} + +func (c *inventoryReceiptController) GetById(ctx *gin.Context) { + id := ctx.Param("id") + receipt, err := c.receiptService.GetById(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusNotFound, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT, receipt) + ctx.JSON(http.StatusOK, res) +} + +func (c *inventoryReceiptController) GetAll(ctx *gin.Context) { + clientId := ctx.DefaultQuery("client_id", "") + var filter query.InventoryReceiptFilter + filter.ClientID = clientId + if err := ctx.ShouldBindQuery(&filter); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + getAll := ctx.Query("get_all") + if getAll != "" { + receipts, _, err := c.receiptService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + response := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT, receipts) + ctx.JSON(http.StatusOK, response) + return + } + perPage := utils.ParseInt(ctx.DefaultQuery("per_page", "10")) + page := utils.ParseInt(ctx.DefaultQuery("page", "1")) + filter.PerPage = perPage + filter.Page = (page - 1) * perPage + receipts, total, err := c.receiptService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + paginationResponse := utils.BuildPaginationResponse(perPage, page, total) + res := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT, receipts, paginationResponse) + ctx.JSON(http.StatusOK, res) +} + +func NewInventoryReceiptController(i *do.Injector, receiptService service.InventoryReceiptService) InventoryReceiptController { + db := do.MustInvokeNamed[*gorm.DB](i, constants.DB) + return &inventoryReceiptController{ + receiptService: receiptService, + db: db, + } +} diff --git a/modules/inventory_receipt/dto/inventory_receipt_dto.go b/modules/inventory_receipt/dto/inventory_receipt_dto.go new file mode 100644 index 0000000..78a6150 --- /dev/null +++ b/modules/inventory_receipt/dto/inventory_receipt_dto.go @@ -0,0 +1,87 @@ +package dto + +import pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + +const ( + MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT = "failed create inventory receipt" + MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT_LINE = "failed create inventory receipt line" + MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT = "success create inventory receipt" + MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT_LINE = "success create inventory receipt line" + MESSAGE_FAILED_GET_INVENTORY_RECEIPT = "failed get inventory receipt" + MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT = "success get inventory receipt" + MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT = "failed update inventory receipt" + MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT_LINE = "failed update inventory receipt line" + MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT = "success update inventory receipt" + MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT_LINE = "success update inventory receipt line" + MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT = "failed delete inventory receipt" + MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT_LINE = "failed delete inventory receipt line" + MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT = "success delete inventory receipt" + MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT_LINE = "success delete inventory receipt line" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" +) + +type InventoryReceiptCreateRequest struct { + ReferenceNumber string `json:"reference_number"` + ReceiptDate string `json:"receipt_date"` + Source string `json:"source"` + QrCodeFile string `json:"qr_code_file"` + ClientID string `json:"client_id" binding:"required"` + ReceiptLines []InventoryReceiptLineCreateRequest `json:"inventory_lines,omitempty" binding:"dive"` +} + +type InventoryReceiptLineCreateRequest struct { + Quantity float64 `json:"quantity"` + BatchNumber string `json:"batch_number"` + RepackingSuggestion string `json:"repacking_suggestion"` + RepackUomID string `json:"repack_uom_id"` + ProductID string `json:"product_id"` + ClientID string `json:"client_id"` +} + +type InventoryReceiptUpdateRequest struct { + ReferenceNumber string `json:"reference_number"` + ReceiptDate string `json:"receipt_date"` + Source string `json:"source"` + QrCodeFile string `json:"qr_code_file"` +} + +type InventoryReceiptResponse struct { + ID string `json:"id"` + ReferenceNumber string `json:"reference_number"` + DocumentNumber string `json:"document_number"` + ReceiptDate string `json:"receipt_date"` + Source string `json:"source"` + QrCodeFile string `json:"qr_code_file"` + Client pkgdto.IdNameResponse `json:"client"` + ReceiptLines []InventoryReceiptLineResponse `json:"inventory_lines"` + LineCount int `json:"line_count"` +} + +type InventoryReceiptLineResponse struct { + ID string `json:"id"` + Quantity float64 `json:"quantity"` + BatchNumber string `json:"batch_number"` + RepackingSuggestion string `json:"repacking_suggestion"` + RepackUomID *string `json:"repack_uom_id"` + Product InventoryReceiptLineProductResponse `json:"product"` + ClientID string `json:"client_id"` +} + +type InventoryReceiptLineProductResponse struct { + ID string `json:"id"` + Name string `json:"name"` + RefNumber string `json:"ref_number"` + Uom pkgdto.IdNameResponse `json:"uom"` + DimLength float64 `json:"dim_length"` + DimWidth float64 `json:"dim_width"` + DimHeight float64 `json:"dim_height"` + DimUom pkgdto.IdNameResponse `json:"dim_uom"` +} + +type InventoryReceiptLineUpdateRequest struct { + Quantity *float64 `json:"quantity"` + BatchNumber *string `json:"batch_number"` + RepackingSuggestion *string `json:"repacking_suggestion"` + RepackUomID *string `json:"repack_uom_id"` + ProductID *string `json:"product_id"` +} diff --git a/modules/inventory_receipt/query/inventory_receipt_query.go b/modules/inventory_receipt/query/inventory_receipt_query.go new file mode 100644 index 0000000..dfc6932 --- /dev/null +++ b/modules/inventory_receipt/query/inventory_receipt_query.go @@ -0,0 +1,22 @@ +package query + +import ( + "gorm.io/gorm" +) + +type InventoryReceiptFilter struct { + ClientID string `form:"client_id"` + Source string `form:"source"` + PerPage int `form:"per_page"` + Page int `form:"page"` +} + +func ApplyInventoryReceiptFilters(db *gorm.DB, filter InventoryReceiptFilter) *gorm.DB { + if filter.ClientID != "" { + db = db.Where("client_id = ?", filter.ClientID) + } + if filter.Source != "" { + db = db.Where("source ILIKE ?", "%"+filter.Source+"%") + } + return db +} diff --git a/modules/inventory_receipt/repository/inventory_receipt_line_repository.go b/modules/inventory_receipt/repository/inventory_receipt_line_repository.go new file mode 100644 index 0000000..6e9309f --- /dev/null +++ b/modules/inventory_receipt/repository/inventory_receipt_line_repository.go @@ -0,0 +1,89 @@ +package repository + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "gorm.io/gorm" +) + +type InventoryReceiptLineRepository interface { + Create(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error) + GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptLineEntity, error) + GetAllByReceiptId(ctx context.Context, receiptId string) ([]entities.TInventoryReceiptLineEntity, error) + Update(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error) + Delete(ctx context.Context, tx *gorm.DB, id string) error + BulkCreate(ctx context.Context, tx *gorm.DB, lines []entities.TInventoryReceiptLineEntity) error + DeleteByReceiptId(ctx context.Context, tx *gorm.DB, receiptId string) error +} + +type inventoryReceiptLineRepository struct { + db *gorm.DB +} + +func NewInventoryReceiptLineRepository(db *gorm.DB) InventoryReceiptLineRepository { + return &inventoryReceiptLineRepository{db: db} +} + +func (r *inventoryReceiptLineRepository) Create(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Create(&line).Error; err != nil { + return line, err + } + return line, nil +} + +func (r *inventoryReceiptLineRepository) GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptLineEntity, error) { + if tx == nil { + tx = r.db + } + var line entities.TInventoryReceiptLineEntity + if err := tx.WithContext(ctx).Preload("Product").Preload("RepackUom").Preload("InvReceipt").Preload("Client").First(&line, "id = ?", id).Error; err != nil { + return line, err + } + return line, nil +} + +func (r *inventoryReceiptLineRepository) GetAllByReceiptId(ctx context.Context, receiptId string) ([]entities.TInventoryReceiptLineEntity, error) { + var lines []entities.TInventoryReceiptLineEntity + if err := r.db.WithContext(ctx).Where("inv_receipt_id = ?", receiptId).Preload("Product").Preload("RepackUom").Preload("InvReceipt").Preload("Client").Find(&lines).Error; err != nil { + return lines, err + } + return lines, nil +} + +func (r *inventoryReceiptLineRepository) Update(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Model(&entities.TInventoryReceiptLineEntity{}).Where("id = ?", line.ID).Select("*").Updates(&line).Error; err != nil { + return line, err + } + return line, nil +} + +func (r *inventoryReceiptLineRepository) Delete(ctx context.Context, tx *gorm.DB, id string) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Delete(&entities.TInventoryReceiptLineEntity{}, "id = ?", id).Error; err != nil { + return err + } + return nil +} + +func (r *inventoryReceiptLineRepository) BulkCreate(ctx context.Context, tx *gorm.DB, lines []entities.TInventoryReceiptLineEntity) error { + if tx == nil { + tx = r.db + } + return tx.WithContext(ctx).Create(&lines).Error +} + +func (r *inventoryReceiptLineRepository) DeleteByReceiptId(ctx context.Context, tx *gorm.DB, receiptId string) error { + if tx == nil { + tx = r.db + } + return tx.WithContext(ctx).Where("inv_receipt_id = ?", receiptId).Delete(&entities.TInventoryReceiptLineEntity{}).Error +} diff --git a/modules/inventory_receipt/repository/inventory_receipt_repository.go b/modules/inventory_receipt/repository/inventory_receipt_repository.go new file mode 100644 index 0000000..f79363c --- /dev/null +++ b/modules/inventory_receipt/repository/inventory_receipt_repository.go @@ -0,0 +1,86 @@ +package repository + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/query" + "gorm.io/gorm" +) + +type InventoryReceiptRepository interface { + Create(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error) + GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptEntity, error) + GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]entities.TInventoryReceiptEntity, int64, error) + Update(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error) + Delete(ctx context.Context, tx *gorm.DB, id string) error +} + +type inventoryReceiptRepository struct { + db *gorm.DB +} + +func NewInventoryReceiptRepository(db *gorm.DB) InventoryReceiptRepository { + return &inventoryReceiptRepository{db: db} +} + +func (r *inventoryReceiptRepository) Create(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Create(&receipt).Error; err != nil { + return receipt, err + } + return receipt, nil +} + +func (r *inventoryReceiptRepository) GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptEntity, error) { + if tx == nil { + tx = r.db + } + var receipt entities.TInventoryReceiptEntity + if err := tx.WithContext(ctx). + Preload("Client"). + Preload("ReceiptLines"). + Preload("ReceiptLines.Product"). + Preload("ReceiptLines.Product.Uom"). + Preload("ReceiptLines.Product.DimUom"). + First(&receipt, "id = ?", id).Error; err != nil { + return receipt, err + } + return receipt, nil +} + +func (r *inventoryReceiptRepository) GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]entities.TInventoryReceiptEntity, int64, error) { + var receipts []entities.TInventoryReceiptEntity + var total int64 + db := query.ApplyInventoryReceiptFilters(r.db, filter) + db.Model(&entities.TInventoryReceiptEntity{}).Count(&total) + if filter.PerPage > 0 && filter.Page > 0 { + db = db.Offset((filter.Page - 1) * filter.PerPage).Limit(filter.PerPage) + } + if err := db.Preload("Client").Find(&receipts).Error; err != nil { + return receipts, total, err + } + return receipts, total, nil +} + +func (r *inventoryReceiptRepository) Update(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Model(&entities.TInventoryReceiptEntity{}).Where("id = ?", receipt.ID).Select("*").Updates(&receipt).Error; err != nil { + return receipt, err + } + return receipt, nil +} + +func (r *inventoryReceiptRepository) Delete(ctx context.Context, tx *gorm.DB, id string) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Delete(&entities.TInventoryReceiptEntity{}, "id = ?", id).Error; err != nil { + return err + } + return nil +} diff --git a/modules/inventory_receipt/routes.go b/modules/inventory_receipt/routes.go new file mode 100644 index 0000000..6c1c41b --- /dev/null +++ b/modules/inventory_receipt/routes.go @@ -0,0 +1,27 @@ +package inventory_receipt + +import ( + "github.com/Caknoooo/go-gin-clean-starter/middlewares" + "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service" + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/controller" + "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" + "github.com/gin-gonic/gin" + "github.com/samber/do" +) + +func RegisterRoutes(server *gin.Engine, injector *do.Injector) { + receiptController := do.MustInvoke[controller.InventoryReceiptController](injector) + jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) + + receiptRoutes := server.Group("/api/v1/inventory-receipts") + { + receiptRoutes.POST("", middlewares.Authenticate(jwtService), receiptController.Create) + receiptRoutes.GET(":id", middlewares.Authenticate(jwtService), receiptController.GetById) + receiptRoutes.PUT(":id", middlewares.Authenticate(jwtService), receiptController.Update) + receiptRoutes.DELETE(":id", middlewares.Authenticate(jwtService), receiptController.Delete) + receiptRoutes.GET("", middlewares.Authenticate(jwtService), receiptController.GetAll) + receiptRoutes.POST(":id/lines", middlewares.Authenticate(jwtService), receiptController.CreateLine) + receiptRoutes.PUT("lines/:id", middlewares.Authenticate(jwtService), receiptController.UpdateLine) + receiptRoutes.DELETE("lines/:id", middlewares.Authenticate(jwtService), receiptController.DeleteLine) + } +} diff --git a/modules/inventory_receipt/service/inventory_receipt_service.go b/modules/inventory_receipt/service/inventory_receipt_service.go new file mode 100644 index 0000000..6fd9212 --- /dev/null +++ b/modules/inventory_receipt/service/inventory_receipt_service.go @@ -0,0 +1,391 @@ +package service + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/repository" + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type InventoryReceiptService interface { + Create(ctx context.Context, req dtodomain.InventoryReceiptCreateRequest) (dtodomain.InventoryReceiptResponse, error) + GetById(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error) + GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]dtodomain.InventoryReceiptResponse, int64, error) + Update(ctx context.Context, req dtodomain.InventoryReceiptUpdateRequest, id string) (dtodomain.InventoryReceiptResponse, error) + Delete(ctx context.Context, id string) error + CreateLine(ctx context.Context, receiptId string, req dtodomain.InventoryReceiptLineCreateRequest) (dtodomain.InventoryReceiptLineResponse, error) + UpdateLine(ctx context.Context, lineId string, req dtodomain.InventoryReceiptLineUpdateRequest) (dtodomain.InventoryReceiptLineResponse, error) + DeleteLine(ctx context.Context, lineId string) error +} + +type inventoryReceiptService struct { + db *gorm.DB + receiptRepo repository.InventoryReceiptRepository + receiptLineRepo repository.InventoryReceiptLineRepository +} + +// DeleteLine implements InventoryReceiptService. +func (s *inventoryReceiptService) DeleteLine(ctx context.Context, lineId string) error { + return s.receiptLineRepo.Delete(ctx, nil, lineId) +} + +// UpdateLine implements InventoryReceiptService. +func (s *inventoryReceiptService) UpdateLine(ctx context.Context, lineId string, req dtodomain.InventoryReceiptLineUpdateRequest) (dtodomain.InventoryReceiptLineResponse, error) { + line, err := s.receiptLineRepo.GetById(ctx, nil, lineId) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + if req.Quantity != nil { + line.Quantity = *req.Quantity + } + if req.BatchNumber != nil { + line.BatchNumber = *req.BatchNumber + } + if req.RepackingSuggestion != nil { + line.RepackingSuggestion = *req.RepackingSuggestion + } + if req.RepackUomID != nil { + if *req.RepackUomID != "" { + tmp, err := uuid.Parse(*req.RepackUomID) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + line.RepackUomID = &tmp + } else { + line.RepackUomID = nil + } + } + if req.ProductID != nil { + if *req.ProductID != "" { + tmp, err := uuid.Parse(*req.ProductID) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + line.ProductID = tmp + } else { + line.ProductID = uuid.Nil + } + } + updated, err := s.receiptLineRepo.Update(ctx, nil, line) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + var repackUomID *string + if updated.RepackUomID != nil { + tmp := updated.RepackUomID.String() + repackUomID = &tmp + } + product := dtodomain.InventoryReceiptLineProductResponse{} + if updated.Product.ID != uuid.Nil { + product = dtodomain.InventoryReceiptLineProductResponse{ + ID: updated.Product.ID.String(), + Name: updated.Product.Name, + } + } + return dtodomain.InventoryReceiptLineResponse{ + ID: updated.ID.String(), + Quantity: updated.Quantity, + BatchNumber: updated.BatchNumber, + RepackingSuggestion: updated.RepackingSuggestion, + RepackUomID: repackUomID, + Product: product, + ClientID: updated.ClientID.String(), + }, nil +} + +func toInventoryReceiptResponse(e entities.TInventoryReceiptEntity) dtodomain.InventoryReceiptResponse { + client := pkgdto.IdNameResponse{} + if e.Client.ID != uuid.Nil { + client = pkgdto.IdNameResponse{ + ID: e.Client.ID.String(), + Name: e.Client.Name, + } + } + + lines := make([]dtodomain.InventoryReceiptLineResponse, 0) + for _, line := range e.ReceiptLines { + product := dtodomain.InventoryReceiptLineProductResponse{} + if line.Product.ID != uuid.Nil { + product = dtodomain.InventoryReceiptLineProductResponse{ + ID: line.Product.ID.String(), + Name: line.Product.Name, + RefNumber: line.Product.RefNumber, + Uom: pkgdto.IdNameResponse{ + ID: line.Product.Uom.ID.String(), + Name: line.Product.Uom.Name, + }, + DimLength: line.Product.DimLength, + DimWidth: line.Product.DimWidth, + DimHeight: line.Product.DimHeight, + DimUom: pkgdto.IdNameResponse{ + ID: line.Product.DimUom.ID.String(), + Name: line.Product.DimUom.Name, + }, + } + } + + var repackUomID *string + if line.RepackUomID != nil { + tmp := line.RepackUomID.String() + repackUomID = &tmp + } else { + repackUomID = nil + } + + lines = append(lines, dtodomain.InventoryReceiptLineResponse{ + ID: line.ID.String(), + Quantity: line.Quantity, + BatchNumber: line.BatchNumber, + RepackingSuggestion: line.RepackingSuggestion, + RepackUomID: repackUomID, + Product: product, + ClientID: line.ClientID.String(), + }) + } + + return dtodomain.InventoryReceiptResponse{ + ID: e.ID.String(), + ReferenceNumber: e.ReferenceNumber, + DocumentNumber: e.DocumentNumber, + ReceiptDate: utils.DateTimeToString(e.ReceiptDate), + Source: e.Source, + QrCodeFile: e.QrCodeFile, + // ClientID: e.ClientID.String(), + Client: client, + LineCount: len(lines), + ReceiptLines: lines, + } +} + +func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.InventoryReceiptCreateRequest) (dtodomain.InventoryReceiptResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + docNum, err := entities.GenerateDocumentNumber(s.db, req.ClientID) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + receipt := entities.TInventoryReceiptEntity{ + ReferenceNumber: req.ReferenceNumber, + DocumentNumber: docNum, + ReceiptDate: utils.StringToDateTime(req.ReceiptDate), + Source: req.Source, + QrCodeFile: req.QrCodeFile, + ClientID: clientUUID, + } + created, err := s.receiptRepo.Create(ctx, tx, receipt) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + // Bulk create lines + var lines []entities.TInventoryReceiptLineEntity + for _, lineReq := range req.ReceiptLines { + var productUUID uuid.UUID + if lineReq.ProductID != "" { + productUUID, err = uuid.Parse(lineReq.ProductID) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + } else { + productUUID = uuid.Nil + } + var repackUomUUID *uuid.UUID + if lineReq.RepackUomID != "" { + tmp, err := uuid.Parse(lineReq.RepackUomID) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + repackUomUUID = &tmp + } else { + repackUomUUID = nil + } + clientLineUUID, err := uuid.Parse(lineReq.ClientID) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + lines = append(lines, entities.TInventoryReceiptLineEntity{ + Quantity: lineReq.Quantity, + BatchNumber: lineReq.BatchNumber, + RepackingSuggestion: lineReq.RepackingSuggestion, + RepackUomID: repackUomUUID, + InvReceiptID: created.ID, + ProductID: productUUID, + ClientID: clientLineUUID, + }) + } + if len(lines) > 0 { + err = s.receiptLineRepo.BulkCreate(ctx, tx, lines) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + } + tx.Commit() + result, err := s.receiptRepo.GetById(ctx, nil, created.ID.String()) + if err != nil { + return dtodomain.InventoryReceiptResponse{}, err + } + return toInventoryReceiptResponse(result), nil +} + +func (s *inventoryReceiptService) GetById(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error) { + receipt, err := s.receiptRepo.GetById(ctx, nil, id) + if err != nil { + return dtodomain.InventoryReceiptResponse{}, err + } + return toInventoryReceiptResponse(receipt), nil +} + +func (s *inventoryReceiptService) GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]dtodomain.InventoryReceiptResponse, int64, error) { + receipts, total, err := s.receiptRepo.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + var responses []dtodomain.InventoryReceiptResponse + for _, e := range receipts { + responses = append(responses, toInventoryReceiptResponse(e)) + } + if responses == nil { + responses = make([]dtodomain.InventoryReceiptResponse, 0) + } + return responses, total, nil +} + +func (s *inventoryReceiptService) Update(ctx context.Context, req dtodomain.InventoryReceiptUpdateRequest, id string) (dtodomain.InventoryReceiptResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + receipt, err := s.receiptRepo.GetById(ctx, tx, id) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + if req.ReferenceNumber != "" { + receipt.ReferenceNumber = req.ReferenceNumber + } + receipt.ReceiptDate = utils.StringToDateTime(req.ReceiptDate) + receipt.Source = req.Source + receipt.QrCodeFile = req.QrCodeFile + updated, err := s.receiptRepo.Update(ctx, tx, receipt) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + tx.Commit() + result, err := s.receiptRepo.GetById(ctx, nil, updated.ID.String()) + if err != nil { + return dtodomain.InventoryReceiptResponse{}, err + } + return toInventoryReceiptResponse(result), nil +} + +func (s *inventoryReceiptService) Delete(ctx context.Context, id string) error { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := s.receiptRepo.Delete(ctx, tx, id); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func (s *inventoryReceiptService) CreateLine(ctx context.Context, receiptId string, req dtodomain.InventoryReceiptLineCreateRequest) (dtodomain.InventoryReceiptLineResponse, error) { + receiptUUID, err := uuid.Parse(receiptId) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + var productUUID uuid.UUID + if req.ProductID != "" { + productUUID, err = uuid.Parse(req.ProductID) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + } else { + productUUID = uuid.Nil + } + var repackUomUUID *uuid.UUID + if req.RepackUomID != "" { + tmp, err := uuid.Parse(req.RepackUomID) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + repackUomUUID = &tmp + } else { + repackUomUUID = nil + } + clientLineUUID, err := uuid.Parse(req.ClientID) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + line := entities.TInventoryReceiptLineEntity{ + Quantity: req.Quantity, + BatchNumber: req.BatchNumber, + RepackingSuggestion: req.RepackingSuggestion, + RepackUomID: repackUomUUID, + InvReceiptID: receiptUUID, + ProductID: productUUID, + ClientID: clientLineUUID, + } + created, err := s.receiptLineRepo.Create(ctx, nil, line) + if err != nil { + return dtodomain.InventoryReceiptLineResponse{}, err + } + var repackUomID *string + if created.RepackUomID != nil { + tmp := created.RepackUomID.String() + repackUomID = &tmp + } + + product := dtodomain.InventoryReceiptLineProductResponse{} + if created.Product.ID != uuid.Nil { + product = dtodomain.InventoryReceiptLineProductResponse{ + ID: created.Product.ID.String(), + Name: created.Product.Name, + } + } + + return dtodomain.InventoryReceiptLineResponse{ + ID: created.ID.String(), + Quantity: created.Quantity, + BatchNumber: created.BatchNumber, + RepackingSuggestion: created.RepackingSuggestion, + RepackUomID: repackUomID, + Product: product, + ClientID: created.ClientID.String(), + }, nil +} + +func NewInventoryReceiptService(db *gorm.DB, receiptRepo repository.InventoryReceiptRepository, receiptLineRepo repository.InventoryReceiptLineRepository) InventoryReceiptService { + return &inventoryReceiptService{ + db: db, + receiptRepo: receiptRepo, + receiptLineRepo: receiptLineRepo, + } +} diff --git a/pkg/utils/conv.go b/pkg/utils/conv.go index bd3bfaf..c933308 100644 --- a/pkg/utils/conv.go +++ b/pkg/utils/conv.go @@ -2,6 +2,7 @@ package utils import ( "strconv" + "time" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" @@ -48,3 +49,45 @@ func ParseUUID(s string) uuid.UUID { } return id } + +// StringToDate parses a string to time.Time with layout "2006-01-02" +func StringToDate(s string) time.Time { + if t, err := time.Parse("2006-01-02", s); err == nil { + now := time.Now() + // Gabungkan tanggal dari input dengan jam:menit:detik dari waktu sekarang + return time.Date( + t.Year(), t.Month(), t.Day(), + now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), + now.Location(), + ) + } + return time.Time{} +} + +// StringToDateTime parses a string to time.Time with layout "2006-01-02T15:04:05Z07:00" +func StringToDateTime(s string) time.Time { + // Coba parse RFC3339 dulu + if t, err := time.Parse(time.RFC3339, s); err == nil { + return t + } + // Jika gagal, coba parse "2006-01-02" + if t, err := time.Parse("2006-01-02", s); err == nil { + now := time.Now() + // Gabungkan tanggal dari input dengan jam:menit:detik dari waktu sekarang + return time.Date( + t.Year(), t.Month(), t.Day(), + now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), + now.Location(), + ) + } + // Jika gagal semua, return zero time + return time.Time{} +} + +func DateToString(t time.Time) string { + return t.Format("2006-01-02") +} + +func DateTimeToString(t time.Time) string { + return t.Format(time.RFC3339) +} diff --git a/providers/core.go b/providers/core.go index 90356f7..e23e345 100644 --- a/providers/core.go +++ b/providers/core.go @@ -58,6 +58,11 @@ import ( aisleRepo "github.com/Caknoooo/go-gin-clean-starter/modules/aisle/repository" aisleService "github.com/Caknoooo/go-gin-clean-starter/modules/aisle/service" + inventoryReceiptController "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/controller" + inventoryReceiptLineRepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/repository" + inventoryReceiptRepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/repository" + inventoryReceiptService "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/service" + "github.com/Caknoooo/go-gin-clean-starter/modules/user/controller" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" @@ -107,6 +112,8 @@ func RegisterDependencies(injector *do.Injector) { warehouseRepository := warehouseRepo.NewWarehouseRepository(db) zonaRepository := zonaRepo.NewZonaRepository(db) aisleRepository := aisleRepo.NewAisleRepository(db) + inventoryReceiptRepository := inventoryReceiptRepo.NewInventoryReceiptRepository(db) + inventoryReceiptLineRepository := inventoryReceiptLineRepo.NewInventoryReceiptLineRepository(db) // Service userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) @@ -122,6 +129,7 @@ func RegisterDependencies(injector *do.Injector) { warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db) zonaServ := zonaService.NewZonaService(zonaRepository, db) aisleServ := aisleService.NewAisleService(aisleRepository, db) + inventoryReceiptServ := inventoryReceiptService.NewInventoryReceiptService(db, inventoryReceiptRepository, inventoryReceiptLineRepository) // Controller do.Provide( @@ -199,4 +207,9 @@ func RegisterDependencies(injector *do.Injector) { return aisleController.NewAisleController(aisleServ), nil }, ) + do.Provide( + injector, func(i *do.Injector) (inventoryReceiptController.InventoryReceiptController, error) { + return inventoryReceiptController.NewInventoryReceiptController(i, inventoryReceiptServ), nil + }, + ) }