Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into fix/BE/US-282/adjustment-recording-egg

This commit is contained in:
ragilap
2025-12-08 12:43:38 +07:00
66 changed files with 2327 additions and 238 deletions
@@ -57,11 +57,11 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid id")
}
result, err := u.ClosingService.GetOne(c, uint(id))
result, err := u.ClosingService.GetProjectFlockByID(c, uint(id))
if err != nil {
return err
}
@@ -70,7 +70,56 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing successfully",
Data: dto.ToClosingListDTO(*result),
Message: "Retrieved closing information successfully",
Data: result,
})
}
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved project information successfully",
Data: result,
})
}
func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
if err != nil {
return err
}
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
})
}
@@ -1,6 +1,7 @@
package dto
import (
"fmt"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -26,6 +27,67 @@ type ClosingDetailDTO struct {
ClosingListDTO
}
type ClosingSummaryDTO struct {
LocationID uint `json:"location_id"`
Periode int `json:"periode"`
JenisProduk string `json:"jenis_produk"`
LabelPopulasi string `json:"label_populasi"`
JumlahPopulasi int `json:"jumlah_populasi"`
JumlahPopulasiFormatted string `json:"jumlah_populasi_formatted"`
JenisProject string `json:"jenis_project"`
KandangAktif int `json:"kandang_aktif"`
KandangAktifFormatted string `json:"kandang_aktif_formatted"`
StatusPembayaranPenjualan string `json:"status_pembayaran_penjualan"`
StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
StatusProject string `json:"status_project"`
StatusClosing string `json:"status_closing"`
}
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
history := project.KandangHistory
period := maxPeriod(history)
kandangCount := len(history)
population := sumPopulation(history)
populationInt := int(population)
return ClosingSummaryDTO{
LocationID: project.LocationId,
Periode: period,
JenisProduk: project.Category,
LabelPopulasi: "",
JumlahPopulasi: populationInt,
JumlahPopulasiFormatted: fmt.Sprintf("%d Ekor", populationInt),
JenisProject: "",
KandangAktif: kandangCount,
KandangAktifFormatted: fmt.Sprintf("%d Kandang", kandangCount),
StatusPembayaranPenjualan: "Tempo",
StatusPembayaranMitra: "",
StatusProject: statusProject,
StatusClosing: statusClosing,
}
}
func maxPeriod(history []entity.ProjectFlockKandang) int {
max := 0
for _, h := range history {
if h.Period > max {
max = h.Period
}
}
return max
}
func sumPopulation(history []entity.ProjectFlockKandang) float64 {
var total float64
for _, h := range history {
for _, chickin := range h.Chickins {
total += chickin.UsageQty + chickin.PendingUsageQty
}
}
return total
}
// === Mapper Functions ===
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
@@ -0,0 +1,109 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
)
// === Response DTO ===
type SalesDTO struct {
Id uint `json:"id"`
RealizationDate time.Time `json:"realization_date"`
Age int `json:"age"`
DoNumber string `json:"do_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AvgWeight float64 `json:"avg_weight"`
Price float64 `json:"price"`
TotalPrice float64 `json:"total_price"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
PaymentStatus string `json:"payment_status"`
}
type PenjualanRealisasiResponseDTO struct {
ProjectType string `json:"project_type"`
FlockId uint `json:"flock_id"`
Period int `json:"period"`
Sales []SalesDTO `json:"sales"`
}
// === Mapper Functions ===
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
// todo: usia ayam masih dummy
age := 0
var product *productDTO.ProductRelationDTO
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
mapped := productDTO.ToProductRelationDTO(e.MarketingProduct.ProductWarehouse.Product)
product = &mapped
}
var customer *customerDTO.CustomerRelationDTO
if e.MarketingProduct.Marketing.Customer.Id != 0 {
mapped := customerDTO.ToCustomerRelationDTO(e.MarketingProduct.Marketing.Customer)
customer = &mapped
}
var kandang *kandangDTO.KandangRelationDTO
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
kandang = &mapped
}
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
return SalesDTO{
Id: e.Id,
RealizationDate: *e.DeliveryDate,
Age: age,
DoNumber: doNumber,
Product: product,
Customer: customer,
Qty: e.Qty,
Weight: e.TotalWeight,
AvgWeight: e.AvgWeight,
Price: e.UnitPrice,
TotalPrice: e.TotalPrice,
Kandang: kandang,
PaymentStatus: "Paid",
}
}
func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
result := make([]SalesDTO, len(e))
for i, r := range e {
result[i] = ToSalesDTO(r)
}
return result
}
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
period := extractPeriodFromRealisasi(e)
return PenjualanRealisasiResponseDTO{
ProjectType: projectType,
FlockId: projectFlockID,
Period: period,
Sales: ToSalesDTOs(e),
}
}
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
if len(realisasi) > 0 {
for _, item := range realisasi {
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
}
}
}
return 0
}
+10 -2
View File
@@ -5,8 +5,12 @@ import (
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -17,10 +21,14 @@ type ClosingModule struct{}
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
closingRepo := rClosing.NewClosingRepository(db)
userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
marketingRepo := rMarketings.NewMarketingRepository(db)
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, validate)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService)
}
+3 -2
View File
@@ -12,7 +12,7 @@ import (
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
ctrl := controller.NewClosingController(s)
route := v1.Group("/closings")
route := v1.Group("/closing")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
@@ -21,5 +21,6 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
}
@@ -1,12 +1,19 @@
package service
import (
"context"
"errors"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -16,20 +23,30 @@ import (
type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
}
type closingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ClosingRepository
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ClosingRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository
MarketingRepo marketingRepository.MarketingRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService
}
func NewClosingService(repo repository.ClosingRepository, validate *validator.Validate) ClosingService {
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
Log: utils.Log,
Validate: validate,
Repository: repo,
ProjectFlockRepo: projectFlockRepo,
MarketingRepo: marketingRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc,
}
}
@@ -37,6 +54,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
return s.withRelations(db).
Preload("KandangHistory").
Preload("KandangHistory.Chickins")
}
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -59,14 +82,109 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
return closings, total, nil
}
func (s closingService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
closing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Closing not found")
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
}
if err != nil {
s.Log.Errorf("Failed get closing by id: %+v", err)
return nil, err
}
return closing, nil
return projectFlock, nil
}
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
})
if err != nil {
return nil, err
}
if len(realisasi) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found")
}
return realisasi, nil
}
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
if err != nil {
s.Log.Errorf("Failed get project flock %d for closing summary: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
}
summary := dto.ToClosingSummaryDTO(*project, statusProject, statusClosing)
return &summary, nil
}
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
if s.ApprovalSvc == nil {
return "", "Belum Selesai", nil
}
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "")
if err != nil {
return "", "", err
}
var (
minStep uint16
statusProject string
completed int
)
for _, rec := range records {
if minStep == 0 || rec.StepNumber < minStep {
minStep = rec.StepNumber
statusProject = rec.StepName
}
if rec.StepNumber == uint16(utils.ProjectFlockStepSelesai) {
completed++
}
}
if statusProject == "" && minStep > 0 {
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, approvalutils.ApprovalStep(minStep)); ok {
statusProject = label
}
}
statusClosing := "Belum Selesai"
switch {
case len(records) == 0 || completed == 0:
statusClosing = "Belum Selesai"
case completed < len(records):
statusClosing = "Sebagian"
default:
statusClosing = "Selesai"
}
return statusProject, statusClosing, nil
}
@@ -151,12 +151,16 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
}
req.Documents = form.File["documents"]
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
transactionDate := c.FormValue("transaction_date")
if transactionDate != "" {
req.TransactionDate = &transactionDate
}
categoryVal := c.FormValue("category")
req.Category = &categoryVal
if categoryVal != "" {
req.Category = &categoryVal
}
supplierIDVal := c.FormValue("supplier_id")
if supplierIDVal != "" {
@@ -312,13 +316,18 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
req.Documents = form.File["documents"]
req.RealizationDate = c.FormValue("realization_date")
realizationDate := c.FormValue("realization_date")
if realizationDate != "" {
req.RealizationDate = &realizationDate
}
realizationsJSON := c.FormValue("realizations")
if realizationsJSON != "" {
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
var realizations []validation.RealizationItem
if err := json.Unmarshal([]byte(realizationsJSON), &realizations); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
}
req.Realizations = &realizations
}
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
+2
View File
@@ -14,6 +14,8 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
route := v1.Group("/expenses")
route.Use(m.Auth(u))
// route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
@@ -732,10 +732,10 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
expenseRepoTx := repository.NewExpenseRepository(tx)
// Check if only updating documents
updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0
updateDataOnly := req.Realizations == nil && len(req.Documents) > 0
if len(req.Realizations) > 0 {
for _, realizationItem := range req.Realizations {
if req.Realizations != nil {
for _, realizationItem := range *req.Realizations {
expenseNonstockID := realizationItem.ExpenseNonstockID
@@ -770,6 +770,12 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
}
if req.RealizationDate != nil {
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{"realization_date": *req.RealizationDate}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
}
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
return err
@@ -5,11 +5,11 @@ import (
)
type Create struct {
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"`
}
@@ -26,11 +26,11 @@ type CostItem struct {
}
type Update struct {
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
}
type Query struct {
@@ -46,9 +46,9 @@ type CreateRealization struct {
}
type UpdateRealization struct {
RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
RealizationDate *string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
Realizations *[]RealizationItem `form:"realizations" json:"realizations" validate:"omitempty,min=1,dive"`
}
type RealizationItem struct {
@@ -104,12 +104,12 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
return AdjustmentRelationDTO{
Id: e.Id,
TransactionType: e.TransactionType,
Quantity: e.Quantity,
BeforeQuantity: e.BeforeQuantity,
AfterQuantity: e.AfterQuantity,
Note: e.Note,
Id: e.Id,
// TransactionType: e.LoggableType,
// Quantity: e.Q,
// BeforeQuantity: e.BeforeQuantity,
// AfterQuantity: e.AfterQuantity,
Note: e.Notes,
ProductWarehouseId: e.ProductWarehouseId,
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
}
@@ -136,6 +136,6 @@ func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
return AdjustmentDetailDTO{
AdjustmentListDTO: ToAdjustmentListDTO(e),
UpdatedAt: e.UpdatedAt,
// UpdatedAt: e.UpdatedAt,
}
}
@@ -66,7 +66,7 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
return nil, err
}
if stockLog.LogType != entity.LogTypeAdjustment {
if stockLog.LoggableType != entity.LogTypeAdjustment {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
@@ -110,7 +110,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID),
Quantity: 0,
CreatedBy: actorID,
// CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
@@ -128,25 +128,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
}
afterQuantity := productWarehouse.Quantity
newLog := &entity.StockLog{
// TransactionType: transactionType,
LoggableType: entity.LogTypeAdjustment,
LoggableId: 0,
Notes: req.Note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: actorID, // TODO: should Get from auth middleware
}
if transactionType == entity.TransactionTypeIncrease {
afterQuantity += req.Quantity
newLog.Increase = afterQuantity
} else {
if productWarehouse.Quantity < req.Quantity {
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
}
afterQuantity -= req.Quantity
}
newLog := &entity.StockLog{
TransactionType: transactionType,
Quantity: req.Quantity,
BeforeQuantity: productWarehouse.Quantity,
AfterQuantity: afterQuantity,
LogType: entity.LogTypeAdjustment,
LogId: 0,
Note: req.Note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: actorID,
newLog.Decrease = afterQuantity
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
@@ -200,7 +198,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
db = s.withRelations(db)
db = db.Where("log_type = ?", entity.LogTypeAdjustment)
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
if query.TransactionType != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
@@ -1,51 +0,0 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type MarketingDeliveryProductRepository interface {
repository.BaseRepository[entity.MarketingDeliveryProduct]
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
}
type MarketingDeliveryProductRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
}
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
return &MarketingDeliveryProductRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
}
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
var deliveryProduct entity.MarketingDeliveryProduct
if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil {
return nil, err
}
return &deliveryProduct, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
// Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct
// Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null)
if err := r.DB().WithContext(ctx).
Preload("MarketingProduct").
Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id").
Where("mp.marketing_id = ?", marketingId).
Where("marketing_delivery_products.delivery_date IS NOT NULL").
Order("marketing_delivery_products.id ASC").
Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
@@ -0,0 +1,77 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
type ProductStockController struct {
ProductStockService service.ProductStockService
}
func NewProductStockController(productStockService service.ProductStockService) *ProductStockController {
return &ProductStockController{
ProductStockService: productStockService,
}
}
func (u *ProductStockController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProductStockService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductStockListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all productStocks successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProductStockListDTOs(result),
})
}
func (u *ProductStockController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
res, err := u.ProductStockService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved product successfully",
Data: dto.ToProductStockDetailDTO(*res),
})
}
@@ -0,0 +1,224 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ProductStockRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductStockListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Brand string `json:"brand"`
Sku *string `json:"sku,omitempty"`
ProductPrice float64 `json:"product_price"`
SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty"`
Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Suppliers []SupplierDTO `json:"suppliers,omitempty"`
ProductWarehouses []ProductWarehouseDTO `json:"product_warehouses,omitempty"`
TotalStock float64 `json:"total_stock"`
}
type ProductStockDetailDTO struct {
ProductStockListDTO
}
type SupplierDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
}
type ProductWarehouseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
Location *locationDTO.LocationRelationDTO `json:"location"`
CurrentStock float64 `json:"current_stock"`
StockLogs []StockLogDetailDTO `json:"stock_logs"`
}
type StockLogDetailDTO struct {
Id uint `json:"id"`
Increase float64 `json:"increase"`
Decrease float64 `json:"decrease"`
LoggableType string `json:"loggable_type"`
LoggableId uint `json:"loggable_id"`
Notes *string `json:"notes"`
ProductWarehouseId uint `json:"product_warehouse_id"`
CreatedBy uint `json:"created_by"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// === Mapper Functions ===
func ToProductStockListDTO(e entity.Product) ProductStockListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
createdUser = &mapped
}
var categoryRef *productCategoryDTO.ProductCategoryRelationDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory)
categoryRef = &mapped
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
var uomRef *uomDTO.UomRelationDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomRelationDTO(e.Uom)
uomRef = &mapped
}
return ProductStockListDTO{
Id: e.Id,
Name: e.Name,
Flags: flags,
Uom: uomRef,
Brand: e.Brand,
Sku: e.Sku,
ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice,
Tax: e.Tax,
ExpiryPeriod: e.ExpiryPeriod,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
ProductCategory: categoryRef,
Suppliers: mapSupplierDTOs(e.ProductSuppliers),
TotalStock: calculateTotalStock(e.ProductWarehouses),
}
}
func ToProductStockListDTOs(e []entity.Product) []ProductStockListDTO {
result := make([]ProductStockListDTO, len(e))
for i, r := range e {
result[i] = ToProductStockListDTO(r)
}
return result
}
func ToProductStockDetailDTO(e entity.Product) ProductStockDetailDTO {
base := ToProductStockListDTO(e)
base.ProductWarehouses = mapProductWarehouseDTOs(e.ProductWarehouses)
return ProductStockDetailDTO{
ProductStockListDTO: base,
}
}
// --- helpers ---
func mapSupplierDTOs(src []entity.ProductSupplier) []SupplierDTO {
if len(src) == 0 {
return nil
}
result := make([]SupplierDTO, 0, len(src))
for _, ps := range src {
if ps.Supplier.Id == 0 {
continue
}
result = append(result, SupplierDTO{
Id: ps.Supplier.Id,
Name: ps.Supplier.Name,
Alias: ps.Supplier.Alias,
Category: ps.Supplier.Category,
})
}
return result
}
func mapProductWarehouseDTOs(src []entity.ProductWarehouse) []ProductWarehouseDTO {
if len(src) == 0 {
return []ProductWarehouseDTO{}
}
result := make([]ProductWarehouseDTO, 0, len(src))
for _, pw := range src {
dto := ProductWarehouseDTO{
Id: pw.Id,
ProductId: pw.ProductId,
WarehouseId: pw.WarehouseId,
CurrentStock: pw.Quantity,
StockLogs: mapStockLogs(pw.StockLogs),
}
if pw.Warehouse.Id != 0 {
dto.WarehouseName = pw.Warehouse.Name
if pw.Warehouse.Location != nil {
mapped := locationDTO.ToLocationRelationDTO(*pw.Warehouse.Location)
dto.Location = &mapped
}
}
result = append(result, dto)
}
return result
}
func mapStockLogs(src []entity.StockLog) []StockLogDetailDTO {
if len(src) == 0 {
return []StockLogDetailDTO{}
}
result := make([]StockLogDetailDTO, 0, len(src))
for _, log := range src {
var notes *string
if log.Notes != "" {
n := log.Notes
notes = &n
}
result = append(result, StockLogDetailDTO{
Id: log.Id,
Increase: log.Increase,
Decrease: log.Decrease,
LoggableType: log.LoggableType,
LoggableId: log.LoggableId,
Notes: notes,
ProductWarehouseId: log.ProductWarehouseId,
CreatedBy: log.CreatedBy,
CreatedUser: mapCreatedUser(log.CreatedUser),
CreatedAt: log.CreatedAt,
})
}
return result
}
func mapCreatedUser(user *entity.User) *userDTO.UserRelationDTO {
if user == nil || user.Id == 0 {
return nil
}
mapped := userDTO.ToUserRelationDTO(*user)
return &mapped
}
func calculateTotalStock(productWarehouses []entity.ProductWarehouse) float64 {
var total float64
for _, pw := range productWarehouses {
total += pw.Quantity
}
return total
}
@@ -0,0 +1,25 @@
package productStocks
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
sProductStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProductStockModule struct{}
func (ProductStockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productRepo := rProduct.NewProductRepository(db)
userRepo := rUser.NewUserRepository(db)
productStockService := sProductStock.NewProductStockService(productRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProductStockRoutes(router, userService, productStockService)
}
@@ -0,0 +1,21 @@
package repository
// import (
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
// "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
// "gorm.io/gorm"
// )
// type ProductStockRepository interface {
// repository.BaseRepository[entity.ProductStock]
// }
// type ProductStockRepositoryImpl struct {
// *repository.BaseRepositoryImpl[entity.ProductStock]
// }
// func NewProductStockRepository(db *gorm.DB) ProductStockRepository {
// return &ProductStockRepositoryImpl{
// BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductStock](db),
// }
// }
@@ -0,0 +1,25 @@
package productStocks
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/controllers"
productStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProductStockRoutes(v1 fiber.Router, u user.UserService, s productStock.ProductStockService) {
ctrl := controller.NewProductStockController(s)
route := v1.Group("/product-stocks")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,91 @@
package service
import (
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
productRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ProductStockService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Product, error)
}
type productStockService struct {
Log *logrus.Logger
Validate *validator.Validate
ProductRepository productRepository.ProductRepository
}
func NewProductStockService(
productRepo productRepository.ProductRepository,
validate *validator.Validate,
) ProductStockService {
return &productStockService{
Log: utils.Log,
Validate: validate,
ProductRepository: productRepo,
}
}
func (s productStockService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Uom").
Preload("ProductCategory").
Preload("Flags").
Preload("ProductWarehouses").
Preload("ProductWarehouses.Warehouse").
Preload("ProductWarehouses.Warehouse.Location").
Preload("ProductWarehouses.Warehouse.Location.Area").
Preload("ProductWarehouses.StockLogs", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at ASC")
}).
Preload("ProductWarehouses.StockLogs.CreatedUser").
Preload("ProductSuppliers").
Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB {
return db.Order("suppliers.name ASC")
})
}
func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get productStocks: %+v", err)
return nil, 0, err
}
return productStocks, total, nil
}
func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
product, err := s.ProductRepository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
if err != nil {
s.Log.Errorf("Failed get product by id: %+v", err)
return nil, err
}
return product, nil
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -98,8 +98,8 @@ func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNeste
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
dto := ProductWarehouseListDTO{
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
// CreatedAt: e.CreatedAt,
// UpdatedAt: e.UpdatedAt,
}
// Map Product relation jika ada
@@ -140,13 +140,13 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
}
// Map CreatedUser relation jika ada
if e.CreatedUser.Id != 0 {
user := UserRelationDTO{
Id: e.CreatedUser.Id,
Username: e.CreatedUser.Name,
}
dto.CreatedUser = &user
}
// if e.CreatedUser.Id != 0 {
// user := UserRelationDTO{
// Id: e.CreatedUser.Id,
// Username: e.CreatedUser.Name,
// }
// dto.CreatedUser = &user
// }
return dto
}
@@ -151,7 +151,7 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
}
if err := base.Model(&entity.ProductWarehouse{}).
Where("id = ?", id).
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
Update("qty", gorm.Expr("COALESCE(qty,0) + ?", delta)).Error; err != nil {
return err
}
}
@@ -171,7 +171,7 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec
var emptyIDs []uint
if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("id IN ? AND COALESCE(quantity,0) <= 0", ids).
Where("id IN ? AND COALESCE(qty,0) <= 0", ids).
Pluck("id", &emptyIDs).Error; err != nil {
return err
}
@@ -213,11 +213,11 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
ProductId: productID,
WarehouseId: warehouseID,
Quantity: 0,
CreatedBy: uint(createdBy),
}
if entity.CreatedBy == 0 {
entity.CreatedBy = 1
// CreatedBy: uint(createdBy),
}
// if entity.CreatedBy == 0 {
// entity.CreatedBy = 1
// }
if err := r.CreateOne(ctx, entity, nil); err != nil {
return 0, err
@@ -257,7 +257,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
Order("product_warehouses.created_at DESC").
Order("product_warehouses.id DESC").
Preload("Product").Preload("Warehouse").
Find(&productWarehouses).Error
if err != nil {
@@ -44,7 +44,7 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Warehouse.Location").
Preload("Warehouse.Area").
Preload("Warehouse.Kandang").
Preload("CreatedUser")
Preload("ProjectFlockKandang")
}
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
@@ -104,7 +104,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
return db.Order("created_at DESC").Order("updated_at DESC")
return db.Order("product_warehouses.id DESC")
})
if err != nil {
+2
View File
@@ -8,6 +8,7 @@ import (
"gorm.io/gorm"
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
productStocks "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks"
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
// MODULE IMPORTS
@@ -21,6 +22,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
adjustments.AdjustmentModule{},
transfers.TransferModule{},
productStocks.ProductStockModule{},
// MODULE REGISTRY
}
@@ -271,15 +271,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
// create stock log for decrease (source)
beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
// beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
decreaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeDecrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeQty,
AfterQuantity: sourcePW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
// TransactionType: entity.TransactionTypeDecrease,
// Quantity: product.ProductQty,
// BeforeQuantity: beforeQty,
// AfterQuantity: sourcePW.Qty,
// LogType: entity.LogTypeTransfer,
// LogId: uint(entityTransfer.Id),
Decrease: product.ProductQty,
Notes: "",
LoggableType: entity.LogTypeTransfer,
LoggableId: uint(entityTransfer.Id),
ProductWarehouseId: sourcePW.Id,
CreatedBy: actorID,
}
@@ -302,7 +305,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0,
CreatedBy: actorID,
// CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
@@ -319,15 +322,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
// create stock log for increase (destination)
beforeDestQty := destPW.Quantity - product.ProductQty
// beforeDestQty := destPW.Quantity - product.ProductQty
increaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeIncrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeDestQty,
AfterQuantity: destPW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
// TransactionType: entity.TransactionTypeIncrease,
// Quantity: product.ProductQty,
// BeforeQuantity: beforeDestQty,
// AfterQuantity: destPW.Qty,
Increase: product.ProductQty,
LoggableType: entity.LogTypeTransfer,
LoggableId: uint(entityTransfer.Id),
Notes: "",
ProductWarehouseId: destPW.Id,
CreatedBy: actorID,
}
@@ -319,15 +319,20 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
})
for i := range groups {
if groups[i].DeliveryDate != nil {
dateStr := groups[i].DeliveryDate.Format("20060102")
groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id)
}
groups[i].DoNumber = GenerateDeliveryOrderNumber(soNumber, groups[i].DeliveryDate, groups[i].Warehouse.Id)
}
return groups
}
func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string {
dateStr := ""
if deliveryDate != nil {
dateStr = deliveryDate.Format("20060102")
}
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId)
}
func getVehicleNumber(e entity.MarketingProduct) string {
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
return e.DeliveryProduct.VehicleNumber
@@ -9,7 +9,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
@@ -22,7 +21,7 @@ type DeliveryOrdersModule struct{}
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
marketingRepo := rMarketing.NewMarketingRepository(db)
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db)
marketingDeliveryProductRepo := rMarketing.NewMarketingDeliveryProductRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
@@ -1,7 +1,7 @@
package delivery_orderss
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -17,6 +17,7 @@ func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.
// Sisanya di group /delivery-orders
route := v1.Group("/delivery-orders")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
@@ -8,9 +8,8 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
@@ -35,14 +34,14 @@ type deliveryOrdersService struct {
Validate *validator.Validate
MarketingRepo marketingRepo.MarketingRepository
MarketingProductRepo marketingRepo.MarketingProductRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService
}
func NewDeliveryOrdersService(
marketingRepo marketingRepo.MarketingRepository,
marketingProductRepo marketingRepo.MarketingProductRepository,
marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository,
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) DeliveryOrdersService {
@@ -200,7 +199,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
@@ -259,7 +258,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
}
approvalAction := entity.ApprovalActionApproved
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
@@ -301,7 +299,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1,6 +1,8 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
@@ -8,6 +10,9 @@ import (
type MarketingDeliveryProductRepository interface {
repository.BaseRepository[entity.MarketingDeliveryProduct]
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
}
type MarketingDeliveryProductRepositoryImpl struct {
@@ -19,3 +24,53 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
}
}
func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
// JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas
// Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter
db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Distinct("marketing_delivery_products.*")
if callback != nil {
db = callback(db)
}
if err := db.Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
// JOIN untuk filter by marketing_id yang ada di related table
db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Where("marketing_products.marketing_id = ?", marketingId)
if err := db.Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
var deliveryProduct entity.MarketingDeliveryProduct
if err := r.DB().WithContext(ctx).
Where("marketing_product_id = ?", marketingProductID).
First(&deliveryProduct).Error; err != nil {
return nil, err
}
return &deliveryProduct, nil
}
@@ -1,7 +1,7 @@
package sales_orders
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -14,6 +14,7 @@ func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesO
v1.Delete("/:id", ctrl.DeleteOne)
route := v1.Group("/sales-orders")
route.Use(m.Auth(u))
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
@@ -10,7 +10,6 @@ import (
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
@@ -125,7 +124,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
marketing = &entity.Marketing{
@@ -220,7 +219,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
updateBody := make(map[string]any)
if req.CustomerId != 0 {
@@ -527,7 +526,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
return updated, nil
}
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error {
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId,
@@ -557,7 +557,7 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId
ProductId: product.Id,
WarehouseId: warehouseId,
Quantity: 0,
CreatedBy: actorID,
// CreatedBy: actorID,
}
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
@@ -329,3 +329,29 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
Message: "Get projectflock kandang successfully",
Data: dtoResult})
}
func (u *ProjectflockController) Resubmit(c *fiber.Ctx) error {
param := c.Params("id")
req := new(validation.Resubmit)
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProjectflockService.Resubmit(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Resubmit projectflock successfully",
Data: dto.ToProjectFlockListDTO(*result),
})
}
@@ -9,6 +9,7 @@ import (
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
@@ -24,15 +25,16 @@ type ProjectFlockRelationDTO struct {
type ProjectFlockListDTO struct {
ProjectFlockRelationDTO
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type KandangWithProjectFlockIdDTO struct {
@@ -51,6 +53,13 @@ type KandangPeriodSummaryDTO struct {
Period int `json:"period"`
}
type ProjectBudgetDTO struct {
Id uint `json:"id"`
Qty float64 `json:"qty"`
Price float64 `json:"price"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
}
func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
@@ -110,6 +119,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
ProjectFlockRelationDTO: createProjectFlockRelationDTO(e, period),
Area: areaSummary,
Kandangs: kandangSummaries,
ProjectBudgets: ToProjectBudgetDTOs(e.Budgets),
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
@@ -184,3 +194,26 @@ func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlo
FlockName: e.FlockName,
}
}
func ToProjectBudgetDTO(e entity.ProjectBudget) ProjectBudgetDTO {
var nonstockRef *nonstockDTO.NonstockRelationDTO
if e.Nonstock != nil && e.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockRelationDTO(*e.Nonstock)
nonstockRef = &mapped
}
return ProjectBudgetDTO{
Id: e.Id,
Qty: e.Qty,
Price: e.Price,
Nonstock: nonstockRef,
}
}
func ToProjectBudgetDTOs(e []entity.ProjectBudget) []ProjectBudgetDTO {
result := make([]ProjectBudgetDTO, len(e))
for i, r := range e {
result[i] = ToProjectBudgetDTO(r)
}
return result
}
@@ -12,7 +12,9 @@ import (
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
@@ -27,10 +29,12 @@ type ProjectflockModule struct{}
func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
flockRepo := rFlock.NewFlockRepository(db)
kandangRepo := rKandang.NewKandangRepository(db)
nonstockRepo := rNonstock.NewNonstockRepository(db)
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
@@ -39,7 +43,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
}
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, approvalService, validate)
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate)
ProjectflockRoutes(router, userService, projectflockService)
@@ -54,7 +54,10 @@ func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm
Preload("Location").
Preload("Kandangs").
Preload("KandangHistory").
Preload("KandangHistory.Kandang")
Preload("KandangHistory.Kandang").
Preload("Budgets").
Preload("Budgets.Nonstock").
Preload("Budgets.Nonstock.Uom")
}
}
@@ -23,5 +23,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval)
route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary)
route.Put("/:id/resubmit", ctrl.Resubmit)
}
@@ -15,7 +15,9 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
nonstockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
projectBudgetRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
@@ -39,6 +41,7 @@ type ProjectflockService interface {
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
Resubmit(ctx *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error)
}
type projectflockService struct {
@@ -47,8 +50,10 @@ type projectflockService struct {
Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository
NonstockRepo nonstockRepository.NonstockRepository
WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
@@ -67,8 +72,11 @@ func NewProjectflockService(
pivotRepo repository.ProjectFlockKandangRepository,
warehouseRepo warehouseRepository.WarehouseRepository,
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
nonstockRepo nonstockRepository.NonstockRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) ProjectflockService {
return &projectflockService{
Log: utils.Log,
@@ -76,6 +84,7 @@ func NewProjectflockService(
Repository: repo,
FlockRepo: flockRepo,
KandangRepo: kandangRepo,
NonstockRepo: nonstockRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PivotRepo: pivotRepo,
@@ -289,7 +298,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction)
// Generate unique flock name (sequential per base name, starting from 1)
generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
if err != nil {
return err
@@ -300,7 +308,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return err
}
// Compute period per kandang so every kandang maintains its own cycle history.
periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs)
if err != nil {
return err
@@ -309,6 +316,10 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return err
}
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, createBody.Id, req.ProjectBudgets); err != nil {
return err
}
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err = approvalSvcTx.CreateApproval(
@@ -1044,3 +1055,139 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
}
return kandangRepository.NewKandangRepository(s.Repository.DB())
}
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
}
kandangIDs := uniqueUintSlice(req.KandangIds)
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data kandang")
}
if len(kandangs) != len(kandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
}
for _, pb := range req.ProjectBudgets {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Nonstock", ID: &pb.NonstockId, Exists: s.NonstockRepo.IdExists},
); err != nil {
return nil, err
}
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
var period int = 1
if len(existing.KandangHistory) > 0 {
period = existing.KandangHistory[0].Period
}
periods := make(map[uint]int, len(kandangIDs))
for _, kandangID := range kandangIDs {
periods[kandangID] = period
}
if err := s.attachKandangs(c.Context(), dbTransaction, existing.Id, kandangIDs, periods); err != nil {
return err
}
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, existing.Id, req.ProjectBudgets); err != nil {
return err
}
action := entity.ApprovalActionUpdated
_, err = approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
existing.Id,
utils.ProjectFlockStepPengajuan,
&action,
actorID,
nil,
)
return err
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengajukan ulang project flock")
}
return s.getOneEntityOnly(c, id)
}
func (s projectflockService) UpsertProjectBudget(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, budgets []validation.ProjectBudget) error {
if len(budgets) == 0 {
return nil
}
budgetRepo := projectBudgetRepository.NewProjectBudgetRepository(dbTransaction)
nonstockMap := make(map[uint]bool)
relationChecks := make([]commonSvc.RelationCheck, 0, len(budgets))
for _, b := range budgets {
if nonstockMap[b.NonstockId] {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate nonstock_id: %d", b.NonstockId))
}
nonstockMap[b.NonstockId] = true
nonstockID := b.NonstockId
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Nonstock",
ID: &nonstockID,
Exists: s.NonstockRepo.IdExists,
})
}
if err := commonSvc.EnsureRelations(ctx, relationChecks...); err != nil {
return err
}
if err := budgetRepo.DeleteMany(ctx, func(q *gorm.DB) *gorm.DB {
return q.Where("project_flock_id = ?", projectFlockID)
}); err != nil && err != gorm.ErrRecordNotFound {
return err
}
records := make([]*entity.ProjectBudget, 0, len(budgets))
for _, b := range budgets {
records = append(records, &entity.ProjectBudget{
ProjectFlockId: projectFlockID,
NonstockId: b.NonstockId,
Price: b.Price,
Qty: b.Qty,
})
}
if err := budgetRepo.CreateMany(ctx, records, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save project budgets")
}
return nil
}
@@ -1,12 +1,13 @@
package validation
type Create struct {
FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
}
type Update struct {
@@ -36,3 +37,14 @@ type Approve struct {
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type ProjectBudget struct {
NonstockId uint `json:"nonstock_id" validate:"required_strict,number,gt=0"`
Price float64 `json:"price" validate:"required_strict,number,gt=0"`
Qty float64 `json:"qty" validate:"required_strict,number,gt=0"`
}
type Resubmit struct {
KandangIds []uint `json:"kandang_ids" validate:"required_strict,min=1,dive,gt=0"`
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required_strict,min=1,dive"`
}
@@ -778,7 +778,7 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
ProductId: productID,
WarehouseId: warehouseID,
Quantity: quantity,
CreatedBy: actorID,
// CreatedBy: actorID,
}
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
@@ -30,7 +30,7 @@ func (r *StockLogRepositoryImpl) GetByFlaggable(ctx context.Context, logType str
var stockLogs []*entity.StockLog
err := r.DB().WithContext(ctx).
Where("log_type = ? AND log_id = ?", logType, logId).
Where("loggable_type = ? AND log_id = ?", logType, logId).
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").