Compare commits

..

20 Commits

Author SHA1 Message Date
giovanni 09c951c16a adjust max limit 2026-01-20 11:49:40 +07:00
Hafizh A. Y. b615570036 Merge branch 'fix/LSS390' into 'development'
[FIX][BE]: LSS390

See merge request mbugroup/lti-api!208
2026-01-20 02:23:42 +00:00
Hafizh A. Y. c793c3cf9a Merge branch 'dev/teguh' into 'development'
FIX[BE]: fixing BE

See merge request mbugroup/lti-api!207
2026-01-20 02:23:29 +00:00
aguhh18 b3e0410f5a Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-20 09:15:21 +07:00
Hafizh A. Y. a882d5a687 Merge branch 'Feat/BE/US-Closing_finance' into 'development'
[FIX][BE]: fixing filter on report penjualan and fix stock movement to not strict to not kandang warehouse

See merge request mbugroup/lti-api!206
2026-01-20 01:41:15 +00:00
Hafizh A. Y. c0848b6d2d Merge branch 'fix/closing-sapronak' into 'development'
[FIX][BE]: query outgoing sapronak

See merge request mbugroup/lti-api!205
2026-01-20 01:40:35 +00:00
aguhh18 b240478ed5 feat[BE]: Add notes field to Update validation and update approval logic in expense services 2026-01-19 17:44:10 +07:00
giovanni 3052497fc0 adjust grouping by project flock kandang 2026-01-19 17:05:43 +07:00
giovanni 71c62c5e02 [FIX][BE]: LSS390 2026-01-19 16:19:47 +07:00
aguhh18 768961d7d6 fix[BE]: Refactor GetAll method to improve query parameter handling and formatting 2026-01-19 14:39:43 +07:00
aguhh18 8cd9627a51 feat[BE]: Add requested_qty field to LayingTransferSource and update related logic for transfer operations 2026-01-19 14:34:08 +07:00
aguhh18 378d633ea4 feat[BE]: Enhance payment allocation logic to support FIFO consumption for sales transactions 2026-01-19 09:27:37 +07:00
aguhh18 fb193fc61f fix[BE]: Update GetAllWithFilters to enhance search functionality and join conditions 2026-01-18 21:26:31 +07:00
aguhh18 af7aabdec8 fix[BE]: Adjust validation for MarketingQuery limit to remove max constraint 2026-01-18 20:50:52 +07:00
aguhh18 bac36b4f00 fix[BE]: Update error messages in TransferService to provide clearer context in Indonesian 2026-01-18 20:36:33 +07:00
aguhh18 687d02313b feat[BE]: Update TransferRelationDTO and service search logic to include warehouse names 2026-01-18 19:46:09 +07:00
aguhh18 7d3602d829 feat[BE]: Enhance CreateOne method to validate project flock closing status and handle warehouse without kandang_id 2026-01-17 13:22:01 +07:00
aguhh18 533e9aca6f FIX[BE]: Fixing filter area and location 2026-01-17 12:20:40 +07:00
giovanni dcfb5e10b4 adjust max limit to 1000 2026-01-17 11:34:12 +07:00
giovanni fbeccf4cdc fix query outgoing sapronak 2026-01-17 11:01:08 +07:00
24 changed files with 513 additions and 291 deletions
@@ -0,0 +1,4 @@
-- Rollback: Remove requested_qty column from laying_transfer_sources table
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS requested_qty;
@@ -0,0 +1,9 @@
-- Add requested_qty column to laying_transfer_sources table
-- This field stores the quantity requested by user during create/update
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
ALTER TABLE laying_transfer_sources
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
-- Add comment for documentation
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
@@ -11,6 +11,7 @@ type LayingTransferSource struct {
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"` SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
@@ -534,6 +534,13 @@ JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pw.warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.project_flock_kandang_id IN ? WHERE pw.project_flock_kandang_id IN ?
AND EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
)
` `
) )
@@ -396,6 +396,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["supplier_id"] = *req.SupplierID updateBody["supplier_id"] = *req.SupplierID
} }
if req.Notes != nil {
updateBody["notes"] = *req.Notes
}
if req.LocationID != nil { if req.LocationID != nil {
locationID := uint(*req.LocationID) locationID := uint(*req.LocationID)
updateBody["location_id"] = locationID updateBody["location_id"] = locationID
@@ -568,20 +572,28 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
} }
if *latestApproval.Action != entity.ApprovalActionUpdated {
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
approvalAction := entity.ApprovalActionUpdated approvalAction := entity.ApprovalActionUpdated
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
if previousStep < utils.ExpenseStepPengajuan {
previousStep = utils.ExpenseStepPengajuan
}
if _, err := approvalSvcTx.CreateApproval( if _, err := approvalSvcTx.CreateApproval(
c.Context(), c.Context(),
utils.ApprovalWorkflowExpense, utils.ApprovalWorkflowExpense,
id, id,
utils.ExpenseStepPengajuan, previousStep,
&approvalAction, &approvalAction,
actorID, actorID,
nil); err != nil { nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step") return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
} }
} }
if s.DocumentSvc != nil && len(req.Documents) > 0 { if s.DocumentSvc != nil && len(req.Documents) > 0 {
@@ -31,6 +31,7 @@ type Update struct {
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"` Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"` SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"` LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"` ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
} }
@@ -4,20 +4,17 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
type TransferRelationDTO struct { type TransferRelationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"` MovementNumber string `json:"movement_number"`
TransferDate string `json:"transfer_date"` TransferReason string `json:"transfer_reason"`
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"` TransferDate string `json:"transfer_date"`
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"` SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
} DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
} }
type ProductSimpleDTO struct { type ProductSimpleDTO struct {
@@ -25,16 +22,6 @@ type ProductSimpleDTO struct {
Name string `json:"name"` Name string `json:"name"`
} }
type AreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type SupplierSimpleDTO struct { type SupplierSimpleDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -48,13 +35,6 @@ type DocumentDTO struct {
Size float64 `json:"size"` Size float64 `json:"size"`
} }
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *LocationDTO `json:"location"`
Area *AreaDTO `json:"area"`
}
type TransferListDTO struct { type TransferListDTO struct {
TransferRelationDTO TransferRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
@@ -97,16 +77,19 @@ type TransferDeliveryItemDTO struct {
} }
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO { func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
var sourceWarehouse *WarehouseDetailDTO var sourceWarehouse *warehouseDTO.WarehouseRelationDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 { if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse) mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse)
sourceWarehouse = &mapped
} }
var destinationWarehouse *WarehouseDetailDTO var destinationWarehouse *warehouseDTO.WarehouseRelationDTO
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 { if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse) mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse)
destinationWarehouse = &mapped
} }
return TransferRelationDTO{ return TransferRelationDTO{
Id: e.Id, Id: e.Id,
MovementNumber: e.MovementNumber,
TransferReason: e.Reason, TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"), TransferDate: e.CreatedAt.Format("2006-01-02"),
SourceWarehouse: sourceWarehouse, SourceWarehouse: sourceWarehouse,
@@ -114,38 +97,6 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
} }
} }
func toAreaDTO(a *entity.Area) *AreaDTO {
if a == nil {
return nil
}
return &AreaDTO{
Id: a.Id,
Name: a.Name,
}
}
func toLocationDTO(l *entity.Location) *LocationDTO {
if l == nil {
return nil
}
return &LocationDTO{
Id: l.Id,
Name: l.Name,
}
}
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
if w == nil {
return nil
}
return &WarehouseDetailDTO{
Id: w.Id,
Name: w.Name,
Location: toLocationDTO(w.Location),
Area: toAreaDTO(&w.Area),
}
}
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var createdUser *userDTO.UserRelationDTO var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil { if e.CreatedUser != nil {
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
route := v1.Group("/transfers") route := v1.Group("/transfers")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
} }
@@ -99,7 +99,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%") searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
searchTerm, searchTerm, searchTerm)
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -118,9 +122,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
}) })
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found") return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer") return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id))
} }
return transferPtr, nil return transferPtr, nil
@@ -136,12 +140,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
) )
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal") return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID))
} }
if sourcePW.Quantity < product.ProductQty { if sourcePW.Quantity < product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
} }
pwIDs = append(pwIDs, sourcePW.Id) pwIDs = append(pwIDs, sourcePW.Id)
} }
@@ -161,10 +165,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID) projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan")
} }
if projectFlockKandang.ClosedAt != nil { if projectFlockKandang.ClosedAt != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing") return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
@@ -192,16 +196,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier") return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID))
} }
if supplier.Category != string(utils.SupplierCategoryBOP) { if supplier.Category != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
} }
} }
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context()) movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number") return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer")
} }
transferDate, _ := utils.ParseDateString(req.TransferDate) transferDate, _ := utils.ParseDateString(req.TransferDate)
@@ -239,16 +243,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
) )
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
} }
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source") return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID))
} }
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID( destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
) )
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination") return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID))
} }
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
ctx := c.Context() ctx := c.Context()
@@ -256,14 +260,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if err != nil { if err != nil {
return err return err
} }
// Set ProjectFlockKandangId hanya jika ada kandang
var pfkID *uint
if projectFlockKandangID > 0 {
pfkID = &projectFlockKandangID
}
destPW = &entity.ProductWarehouse{ destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID), ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID), WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0, Quantity: 0,
ProjectFlockKandangId: &projectFlockKandangID, ProjectFlockKandangId: pfkID,
} }
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil { if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination") return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID))
} }
} }
@@ -309,7 +320,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
for _, prod := range item.Products { for _, prod := range item.Products {
detail, ok := detailMap[uint64(prod.ProductID)] detail, ok := detailMap[uint64(prod.ProductID)]
if !ok { if !ok {
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
} }
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{ deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
StockTransferDeliveryId: delivery.Id, StockTransferDeliveryId: delivery.Id,
@@ -372,7 +383,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx, Tx: tx,
}) })
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
} }
if err := tx.Model(&entity.StockTransferDetail{}). if err := tx.Model(&entity.StockTransferDetail{}).
@@ -381,7 +392,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
"usage_qty": consumeResult.UsageQuantity, "usage_qty": consumeResult.UsageQuantity,
"pending_qty": consumeResult.PendingQuantity, "pending_qty": consumeResult.PendingQuantity,
}).Error; err != nil { }).Error; err != nil {
return fmt.Errorf("gagal update usage tracking: %w", err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID))
} }
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
@@ -394,7 +405,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx, Tx: tx,
}) })
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err))
} }
if err := tx.Model(&entity.StockTransferDetail{}). if err := tx.Model(&entity.StockTransferDetail{}).
@@ -402,7 +413,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"total_qty": replenishResult.AddedQuantity, "total_qty": replenishResult.AddedQuantity,
}).Error; err != nil { }).Error; err != nil {
return fmt.Errorf("gagal update total tracking: %w", err) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID))
} }
} }
@@ -436,7 +447,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}) })
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err)) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err))
} }
result, err := s.GetOne(c, uint(entityTransfer.Id)) result, err := s.GetOne(c, uint(entityTransfer.Id))
@@ -446,8 +457,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if len(expensePayloads) > 0 { if len(expensePayloads) > 0 {
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil { if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber))
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
} }
} }
@@ -461,32 +471,26 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads) return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
} }
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
return nil
}
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
}
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) { func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil) warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID)) return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
} }
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang") return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID))
} }
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
if warehouse.KandangId == nil || *warehouse.KandangId == 0 { if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID)) return 0, nil
} }
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId)) projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId)) return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
} }
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang") return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif")
} }
return uint(projectFlockKandang.Id), nil return uint(projectFlockKandang.Id), nil
@@ -140,12 +140,21 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
if actorID == 0 { if actorID == 0 {
actorID = 1 actorID = 1
} }
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) approvalRepo := commonRepo.NewApprovalRepository(b.db)
action := entity.ApprovalActionUpdated svc := commonSvc.NewApprovalService(approvalRepo)
action := entity.ApprovalActionCreated
for id := range expenseIDs { for id := range expenseIDs {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err return err
} }
if latestApproval == nil {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
} }
return nil return nil
} }
@@ -140,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
Where("marketing_delivery_products.delivery_date IS NOT NULL") Where("marketing_delivery_products.delivery_date IS NOT NULL")
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" { if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
} }
@@ -148,18 +148,30 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
} }
if filters.WarehouseId > 0 { if filters.WarehouseId > 0 || filters.Search != "" {
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
} }
if filters.Search != "" { if filters.Search != "" {
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
} db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id")
if filters.Search != "" {
searchPattern := "%" + filters.Search + "%" searchPattern := "%" + filters.Search + "%"
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?", db = db.Where(`(
searchPattern, searchPattern, searchPattern, searchPattern) marketing_delivery_products.vehicle_number ILIKE ? OR
customers.name ILIKE ? OR
warehouses.name ILIKE ? OR
products.name ILIKE ? OR
sales_users.name ILIKE ? OR
CONCAT(
marketings.so_number,
'-',
COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''),
'-',
COALESCE(product_warehouses.warehouse_id::text, '')
) ILIKE ?
)`,
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
} }
if filters.CustomerId > 0 { if filters.CustomerId > 0 {
@@ -178,6 +190,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
} }
if filters.AreaId > 0 || filters.LocationId > 0 {
db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
if filters.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
}
if filters.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
}
}
if filters.MarketingType != "" { if filters.MarketingType != "" {
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Group("marketing_delivery_products.id") Group("marketing_delivery_products.id")
@@ -186,7 +211,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
case "ayam": case "ayam":
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer), string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
}) })
case "telur": case "telur":
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
@@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
newChikins = append(newChikins, newChickin) newChikins = append(newChikins, newChickin)
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId)) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId))
} }
availableQty := productWarehouse.Quantity - totalPopulationQty availableQty := productWarehouse.Quantity - totalPopulationQty
@@ -1,6 +1,7 @@
package dto package dto
import ( import (
"strconv"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -53,6 +54,7 @@ type ProjectFlockKandangListDTO struct {
ProjectFlockKandangRelationDTO ProjectFlockKandangRelationDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
NameWithPeriod string `json:"name_with_period"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"` Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
@@ -104,6 +106,7 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary), ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang), Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock), CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }), Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -126,6 +129,16 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
return &mapped return &mapped
} }
func toNameWithPeriod(kandang entity.Kandang, period int) string {
if kandang.Name == "" {
return ""
}
if period == 0 {
return kandang.Name
}
return kandang.Name + " Period " + strconv.Itoa(period)
}
func toApprovalDTOSelector( func toApprovalDTOSelector(
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO { e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
approval := selector(e) approval := selector(e)
@@ -147,6 +160,7 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e), ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary), ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang), Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock), CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }), Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"math"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -16,6 +17,7 @@ type ProjectFlockPopulationRepository interface {
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error)
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
@@ -111,7 +113,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(c
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlockPopulation{}). Model(&entity.ProjectFlockPopulation{}).
Where("product_warehouse_id = ?", productWarehouseID). Where("product_warehouse_id = ?", productWarehouseID).
Select("COALESCE(SUM(total_qty), 0)"). Select("COALESCE(SUM(total_qty - total_used_qty), 0)").
Scan(&total).Error Scan(&total).Error
if err != nil { if err != nil {
return 0, err return 0, err
@@ -135,3 +137,22 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand
} }
return total, nil return total, nil
} }
func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) {
var total float64
err := r.DB().WithContext(ctx).
Table("project_flock_populations").
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Scan(&total).Error
if err != nil {
return 0, err
}
if total < 0 {
total = 0
}
return int64(math.Round(total)), nil
}
@@ -25,8 +25,12 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
TransferDate: c.Query("transfer_date", ""),
FlockSource: uint(c.QueryInt("flock_source", 0)),
FlockDestination: uint(c.QueryInt("flock_destination", 0)),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -179,7 +183,6 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
}) })
} }
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error { func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32) projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
if err != nil { if err != nil {
@@ -162,9 +162,19 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
} }
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
// Tampilkan requested qty sebelum approve, consumed qty setelah approve
var displayQty float64
if source.UsageQty > 0 {
// Sudah di-approve dan di-consume, tampilkan actual consumed quantity
displayQty = source.UsageQty
} else {
// Belum di-approve, tampilkan requested quantity
displayQty = source.RequestedQty
}
return LayingTransferSourceDTO{ return LayingTransferSourceDTO{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity) Qty: displayQty,
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note, Note: source.Note,
} }
@@ -110,8 +110,31 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) // Apply search and filters
if params.Search != "" {
searchPattern := "%" + params.Search + "%"
db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
searchPattern, searchPattern, searchPattern, searchPattern)
}
if params.TransferDate != "" {
db = db.Where("transfer_date::date = ?::date", params.TransferDate)
}
if params.FlockSource > 0 {
db = db.Where("from_project_flock_id = ?", params.FlockSource)
}
if params.FlockDestination > 0 {
db = db.Where("to_project_flock_id = ?", params.FlockDestination)
}
db = db.Order("created_at DESC") db = db.Order("created_at DESC")
db = s.withRelations(db)
return db return db
}) })
@@ -216,7 +239,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 { if sourceDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0") continue
} }
totalSourceQty += sourceDetail.Quantity totalSourceQty += sourceDetail.Quantity
@@ -247,11 +270,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, targetDetail := range req.TargetKandangs { for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 { if targetDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0") continue
} }
totalTargetQty += targetDetail.Quantity totalTargetQty += targetDetail.Quantity
} }
if totalSourceQty == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0")
}
if totalTargetQty == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
}
if totalSourceQty != totalTargetQty { if totalSourceQty != totalTargetQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
} }
@@ -279,11 +309,16 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
} }
for _, sourceDetail := range req.SourceKandangs { for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity == 0 {
continue
}
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: createBody.Id, LayingTransferId: createBody.Id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
UsageQty: 0, UsageQty: 0,
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
ProductWarehouseId: &productWarehouseId, ProductWarehouseId: &productWarehouseId,
@@ -295,6 +330,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
} }
for _, targetDetail := range req.TargetKandangs { for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity == 0 {
continue
}
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil { if err != nil {
@@ -463,8 +501,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
source := entity.LayingTransferSource{ source := entity.LayingTransferSource{
LayingTransferId: id, LayingTransferId: id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
UsageQty: 0, UsageQty: 0,
PendingUsageQty: sourceDetail.Quantity, PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
ProductWarehouseId: &productWarehouseId, ProductWarehouseId: &productWarehouseId,
} }
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil { if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
@@ -700,7 +739,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
} }
note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber) note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyTransferToLayingIn, StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id, StockableID: target.Id,
@@ -814,15 +853,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
kandangAvailableQty := make(map[uint]float64) kandangAvailableQty := make(map[uint]float64)
for _, kandang := range kandangs { for _, kandang := range kandangs {
// Gunakan fungsi repository yang sama dengan recording service
totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
if err != nil { if err != nil {
s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err) s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err)
kandangAvailableQty[kandang.Id] = 0 kandangAvailableQty[kandang.Id] = 0
continue continue
} }
kandangAvailableQty[kandang.Id] = totalQty kandangAvailableQty[kandang.Id] = totalAvailable
} }
return pf, kandangAvailableQty, nil return pf, kandangAvailableQty, nil
@@ -2,12 +2,12 @@ package validation
type SourceKandangDetail struct { type SourceKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"` Quantity float64 `json:"quantity"`
} }
type TargetKandangDetail struct { type TargetKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"` Quantity float64 `json:"quantity"`
} }
type Create struct { type Create struct {
@@ -29,8 +29,12 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty"`
TransferDate string `query:"transfer_date" validate:"omitempty"`
FlockSource uint `query:"flock_source" validate:"omitempty,number"`
FlockDestination uint `query:"flock_destination" validate:"omitempty,number"`
} }
type Approve struct { type Approve struct {
@@ -167,12 +167,21 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[
if actorID == 0 { if actorID == 0 {
actorID = 1 actorID = 1
} }
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) approvalRepo := commonRepo.NewApprovalRepository(b.db)
action := entity.ApprovalActionUpdated svc := commonSvc.NewApprovalService(approvalRepo)
action := entity.ApprovalActionCreated
for id := range expenseIDs { for id := range expenseIDs {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err return err
} }
if latestApproval == nil {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
} }
return nil return nil
} }
@@ -82,6 +82,8 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
ProductId: int64(ctx.QueryInt("product_id", 0)), ProductId: int64(ctx.QueryInt("product_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
AreaId: int64(ctx.QueryInt("area_id", 0)),
LocationId: int64(ctx.QueryInt("location_id", 0)),
MarketingType: ctx.Query("marketing_type", ""), MarketingType: ctx.Query("marketing_type", ""),
FilterBy: ctx.Query("filter_by", ""), FilterBy: ctx.Query("filter_by", ""),
StartDate: ctx.Query("start_date", ""), StartDate: ctx.Query("start_date", ""),
@@ -27,10 +27,13 @@ type HppPerKandangResponseData struct {
type HppPerKandangRowDTO struct { type HppPerKandangRowDTO struct {
ID int `json:"id"` ID int `json:"id"`
Kandang HppPerKandangRowKandangDTO `json:"kandang"` Kandang HppPerKandangRowKandangDTO `json:"kandang"`
NameWithPeriode string `json:"name_with_periode"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"` WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
AvgWeightKg float64 `json:"avg_weight_kg"` AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"` EggProductionPieces int64 `json:"egg_production_pieces"`
EggProductionKg float64 `json:"egg_production_kg"` EggProductionKg float64 `json:"egg_production_kg"`
// EggProductionTotalWeightKg float64 `json:"egg_production_total_weight_kg"`
// EggProductionTotalPieces int64 `json:"egg_production_total_pieces"`
// FeedCostRp float64 `json:"feed_cost_rp"` // FeedCostRp float64 `json:"feed_cost_rp"`
// OvkCostRp float64 `json:"ovk_cost_rp"` // OvkCostRp float64 `json:"ovk_cost_rp"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"` EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
@@ -38,8 +41,8 @@ type HppPerKandangRowDTO struct {
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"` FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"` DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp int64 `json:"average_doc_price_rp"` AverageDocPriceRp int64 `json:"average_doc_price_rp"`
HppRp float64 `json:"hpp_rp"` // HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"` // RemainingValueRp int64 `json:"remaining_value_rp"`
} }
type HppPerKandangRowKandangDTO struct { type HppPerKandangRowKandangDTO struct {
@@ -11,37 +11,40 @@ import (
) )
type HppPerKandangRow struct { type HppPerKandangRow struct {
ProjectFlockKandangID uint ProjectFlockKandangID uint
KandangID uint ProjectFlockPeriod int
KandangName string KandangID uint
KandangStatus string KandangName string
LocationID uint KandangStatus string
LocationName string LocationID uint
PicID uint LocationName string
PicName string PicID uint
RecordingCount int64 PicName string
RemainingChickenBirds float64 RecordingCount int64
RemainingChickenWeight float64 // RemainingChickenBirds float64
EggProductionWeightKg float64 // RemainingChickenWeight float64
EggProductionPieces float64 EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
} }
type HppPerKandangCostRow struct { type HppPerKandangCostRow struct {
KandangID uint ProjectFlockKandangID uint
FeedCost float64 FeedCost float64
OvkCost float64 OvkCost float64
DocCost float64 DocCost float64
DocQty float64 DocQty float64
BudgetCost float64 BudgetCost float64
ExpenseCost float64 ExpenseCost float64
} }
type HppPerKandangSupplierRow struct { type HppPerKandangSupplierRow struct {
KandangID uint ProjectFlockKandangID uint
SupplierID uint SupplierID uint
SupplierName string SupplierName string
SupplierAlias string SupplierAlias string
Category string Category string
} }
type HppPerKandangRepository interface { type HppPerKandangRepository interface {
@@ -86,6 +89,7 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
Table("project_flocks AS pf"). Table("project_flocks AS pf").
Select(` Select(`
pfk.id AS project_flock_kandang_id, pfk.id AS project_flock_kandang_id,
pfk.period AS project_flock_period,
k.id AS kandang_id, k.id AS kandang_id,
k.name AS kandang_name, k.name AS kandang_name,
k.status AS kandang_status, k.status AS kandang_status,
@@ -97,18 +101,27 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds, COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds,
0 AS remaining_chicken_weight, 0 AS remaining_chicken_weight,
0 AS egg_production_weight_kg, 0 AS egg_production_weight_kg,
0 AS egg_production_pieces`). 0 AS egg_production_pieces,
0 AS egg_production_total_weight_kg,
0 AS egg_production_total_pieces`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins(`
LEFT JOIN (
SELECT project_flock_kandang_id, MIN(chick_in_date) AS chick_in_date
FROM project_chickins
GROUP BY project_flock_kandang_id
) AS pc ON pc.project_flock_kandang_id = pfk.id`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id"). Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN users AS pic ON pic.id = k.pic_id"). Joins("JOIN users AS pic ON pic.id = k.pic_id").
Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings). Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings).
Where("pfk.closed_at IS NULL") Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
Where("(pfk.closed_at IS NULL OR ? BETWEEN pc.chick_in_date AND pfk.closed_at)", start)
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs) query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
query = query.Group("pfk.id, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name"). query = query.Group("pfk.id, pfk.period, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
Order("k.id ASC") Order("pfk.id ASC")
if err := query.Scan(&rows).Error; err != nil { if err := query.Scan(&rows).Error; err != nil {
return nil, err return nil, err
@@ -139,7 +152,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select(` Select(`
k.id AS kandang_id, pfk.id AS project_flock_kandang_id,
COALESCE(SUM(CASE COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
@@ -164,29 +177,29 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs). Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL"). Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)) Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
query = query.Group("k.id").Order("k.id ASC") query = query.Group("pfk.id").Order("pfk.id ASC")
if err := query.Scan(&rows).Error; err != nil { if err := query.Scan(&rows).Error; err != nil {
return nil, nil, err return nil, nil, err
} }
docRows := make([]struct { docRows := make([]struct {
KandangID uint ProjectFlockKandangID uint
DocCost float64 DocCost float64
DocQty float64 DocQty float64
SupplierID *uint SupplierID *uint
SupplierName *string SupplierName *string
SupplierAlias *string SupplierAlias *string
}, 0) }, 0)
docQuery := r.db.WithContext(ctx). docQuery := r.db.WithContext(ctx).
Table("project_chickins AS pc"). Table("project_chickins AS pc").
Select(` Select(`
pfk.kandang_id AS kandang_id, pfk.id AS project_flock_kandang_id,
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost, COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty, COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
s.id AS supplier_id, s.id AS supplier_id,
@@ -199,7 +212,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id"). Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id"). Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs). Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Group("pfk.kandang_id, s.id, s.name, s.alias") Group("pfk.id, s.id, s.name, s.alias")
if err := docQuery.Scan(&docRows).Error; err != nil { if err := docQuery.Scan(&docRows).Error; err != nil {
return nil, nil, err return nil, nil, err
@@ -208,28 +221,28 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
costMap := make(map[uint]*HppPerKandangCostRow, len(rows)) costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
for i := range rows { for i := range rows {
row := rows[i] row := rows[i]
costMap[row.KandangID] = &rows[i] costMap[row.ProjectFlockKandangID] = &rows[i]
} }
docSuppliers := make([]HppPerKandangSupplierRow, 0) docSuppliers := make([]HppPerKandangSupplierRow, 0)
docSeen := make(map[uint]map[uint]bool) docSeen := make(map[uint]map[uint]bool)
for _, doc := range docRows { for _, doc := range docRows {
entry, ok := costMap[doc.KandangID] entry, ok := costMap[doc.ProjectFlockKandangID]
if !ok { if !ok {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
KandangID: doc.KandangID, ProjectFlockKandangID: doc.ProjectFlockKandangID,
}) })
entry = &rows[len(rows)-1] entry = &rows[len(rows)-1]
costMap[doc.KandangID] = entry costMap[doc.ProjectFlockKandangID] = entry
} }
entry.DocCost += doc.DocCost entry.DocCost += doc.DocCost
entry.DocQty += doc.DocQty entry.DocQty += doc.DocQty
if doc.SupplierID != nil { if doc.SupplierID != nil {
if docSeen[doc.KandangID] == nil { if docSeen[doc.ProjectFlockKandangID] == nil {
docSeen[doc.KandangID] = make(map[uint]bool) docSeen[doc.ProjectFlockKandangID] = make(map[uint]bool)
} }
if !docSeen[doc.KandangID][*doc.SupplierID] { if !docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] {
docSeen[doc.KandangID][*doc.SupplierID] = true docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] = true
supplierName := "" supplierName := ""
if doc.SupplierName != nil { if doc.SupplierName != nil {
supplierName = *doc.SupplierName supplierName = *doc.SupplierName
@@ -239,19 +252,19 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
supplierAlias = *doc.SupplierAlias supplierAlias = *doc.SupplierAlias
} }
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{ docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
KandangID: doc.KandangID, ProjectFlockKandangID: doc.ProjectFlockKandangID,
SupplierID: *doc.SupplierID, SupplierID: *doc.SupplierID,
SupplierName: supplierName, SupplierName: supplierName,
SupplierAlias: supplierAlias, SupplierAlias: supplierAlias,
Category: "DOC", Category: "DOC",
}) })
} }
} }
} }
budgetRows := make([]struct { budgetRows := make([]struct {
KandangID uint ProjectFlockKandangID uint
BudgetCost float64 BudgetCost float64
}, 0) }, 0)
pfkUsageSub := r.db. pfkUsageSub := r.db.
@@ -272,7 +285,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
budgetQuery := r.db.WithContext(ctx). budgetQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk"). Table("project_flock_kandangs AS pfk").
Select(` Select(`
k.id AS kandang_id, pfk.id AS project_flock_kandang_id,
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`). COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id"). Joins("JOIN locations AS loc ON loc.id = k.location_id").
@@ -280,7 +293,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub). Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub). Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
Where("pfk.id IN (?)", projectFlockKandangIDs). Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("k.id") Group("pfk.id")
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs) // budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
if err := budgetQuery.Scan(&budgetRows).Error; err != nil { if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
@@ -288,33 +301,33 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
} }
for _, budget := range budgetRows { for _, budget := range budgetRows {
entry, ok := costMap[budget.KandangID] entry, ok := costMap[budget.ProjectFlockKandangID]
if !ok { if !ok {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
KandangID: budget.KandangID, ProjectFlockKandangID: budget.ProjectFlockKandangID,
}) })
entry = &rows[len(rows)-1] entry = &rows[len(rows)-1]
costMap[budget.KandangID] = entry costMap[budget.ProjectFlockKandangID] = entry
} }
entry.BudgetCost += budget.BudgetCost entry.BudgetCost += budget.BudgetCost
} }
expenseRows := make([]struct { expenseRows := make([]struct {
KandangID uint ProjectFlockKandangID uint
ExpenseCost float64 ExpenseCost float64
}, 0) }, 0)
expenseQuery := r.db.WithContext(ctx). expenseQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk"). Table("project_flock_kandangs AS pfk").
Select(` Select(`
k.id AS kandang_id, pfk.id AS project_flock_kandang_id,
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`). COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id"). Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id"). Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id"). Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
Where("pfk.id IN (?)", projectFlockKandangIDs). Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("k.id") Group("pfk.id")
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs) // expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
if err := expenseQuery.Scan(&expenseRows).Error; err != nil { if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
@@ -322,13 +335,13 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
} }
for _, exp := range expenseRows { for _, exp := range expenseRows {
entry, ok := costMap[exp.KandangID] entry, ok := costMap[exp.ProjectFlockKandangID]
if !ok { if !ok {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
KandangID: exp.KandangID, ProjectFlockKandangID: exp.ProjectFlockKandangID,
}) })
entry = &rows[len(rows)-1] entry = &rows[len(rows)-1]
costMap[exp.KandangID] = entry costMap[exp.ProjectFlockKandangID] = entry
} }
entry.ExpenseCost += exp.ExpenseCost entry.ExpenseCost += exp.ExpenseCost
} }
@@ -337,7 +350,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
feedQuery := r.db.WithContext(ctx). feedQuery := r.db.WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select("DISTINCT k.id AS kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias"). Select("DISTINCT pfk.id AS project_flock_kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id"). Joins("JOIN locations AS loc ON loc.id = k.location_id").
@@ -349,7 +362,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}). Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL") Where("r.deleted_at IS NULL")
// feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs) // feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
@@ -358,11 +371,11 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
} }
for i := range feedSuppliers { for i := range feedSuppliers {
if _, exists := costMap[feedSuppliers[i].KandangID]; !exists { if _, exists := costMap[feedSuppliers[i].ProjectFlockKandangID]; !exists {
rows = append(rows, HppPerKandangCostRow{ rows = append(rows, HppPerKandangCostRow{
KandangID: feedSuppliers[i].KandangID, ProjectFlockKandangID: feedSuppliers[i].ProjectFlockKandangID,
}) })
costMap[feedSuppliers[i].KandangID] = &rows[len(rows)-1] costMap[feedSuppliers[i].ProjectFlockKandangID] = &rows[len(rows)-1]
} }
feedSuppliers[i].Category = "FEED" feedSuppliers[i].Category = "FEED"
} }
@@ -391,9 +404,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
) )
type eggRow struct { type eggRow struct {
ProjectFlockKandangID uint ProjectFlockKandangID uint
EggProductionWeightKg float64 EggProductionWeightKgRemaining float64
EggProductionPieces float64 EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
} }
eggRows := make([]eggRow, 0) eggRows := make([]eggRow, 0)
@@ -401,12 +416,14 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
Table("recordings AS r"). Table("recordings AS r").
Select(` Select(`
r.project_flock_kandangs_id AS project_flock_kandang_id, r.project_flock_kandangs_id AS project_flock_kandang_id,
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg, COALESCE(SUM((re.total_qty - re.total_used) * re.weight / 1000), 0) AS egg_production_weight_kg_remaining,
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`). COALESCE(SUM(re.total_qty - re.total_used), 0) AS egg_production_pieces_remaining,
COALESCE(SUM(re.weight / 1000), 0) AS egg_production_total_weight_kg,
COALESCE(SUM(re.total_qty), 0) AS egg_production_total_pieces`).
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval). Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id"). Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs). Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). Where("r.record_datetime < ?", end).
Where("r.deleted_at IS NULL"). Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Group("r.project_flock_kandangs_id") Group("r.project_flock_kandangs_id")
@@ -418,9 +435,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
result := make(map[uint]HppPerKandangRow, len(eggRows)) result := make(map[uint]HppPerKandangRow, len(eggRows))
for _, row := range eggRows { for _, row := range eggRows {
result[row.ProjectFlockKandangID] = HppPerKandangRow{ result[row.ProjectFlockKandangID] = HppPerKandangRow{
ProjectFlockKandangID: row.ProjectFlockKandangID, ProjectFlockKandangID: row.ProjectFlockKandangID,
EggProductionWeightKg: row.EggProductionWeightKg, EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
EggProductionPieces: row.EggProductionPieces, EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
EggProductionTotalPieces: row.EggProductionTotalPieces,
} }
} }
@@ -435,7 +454,7 @@ func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int
query = query.Where("k.location_id IN ?", locationIDs) query = query.Where("k.location_id IN ?", locationIDs)
} }
if len(kandangIDs) > 0 { if len(kandangIDs) > 0 {
query = query.Where("k.id IN ?", kandangIDs) query = query.Where("pfk.id IN ?", kandangIDs)
} }
return query return query
} }
@@ -464,9 +464,13 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
result = append(result, item)
if len(item.Rows) > 0 {
result = append(result, item)
}
} }
totalCustomers = int64(len(result))
return result, totalCustomers, nil return result, totalCustomers, nil
} }
@@ -503,14 +507,8 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
row.Status = status row.Status = status
if status == "LUNAS" { if status == "LUNAS" {
if previousBalance >= tx.TotalPrice { if paymentDate != nil {
days := 0
row.AgingDay = &days
} else if paymentDate != nil {
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24) days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days row.AgingDay = &days
} else { } else {
days := 0 days := 0
@@ -518,9 +516,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
} }
} else { } else {
days := int(time.Since(tx.TransDate).Hours() / 24) days := int(time.Since(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days row.AgingDay = &days
} }
} else if tx.TransactionType == "PAYMENT" { } else if tx.TransactionType == "PAYMENT" {
@@ -586,6 +581,67 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
// 4. BELUM LUNAS: no payment at all // 4. BELUM LUNAS: no payment at all
if previousBalance >= currentSales.TotalPrice { if previousBalance >= currentSales.TotalPrice {
// Cari payment yang digunakan untuk melunasi sales ini dengan FIFO
// Track payment allocations that are consumed by previous sales
type paymentAllocation struct {
date time.Time
amount float64
consumed float64
}
allocations := []paymentAllocation{}
runningBalance := 0.0
// Process all transactions before current sales to build allocation map
for i := 0; i < currentIndex; i++ {
if transactions[i].TransactionType == "PAYMENT" {
allocations = append(allocations, paymentAllocation{
date: transactions[i].TransDate,
amount: transactions[i].PaymentAmount,
consumed: 0,
})
runningBalance += transactions[i].PaymentAmount
} else if transactions[i].TransactionType == "SALES" {
salesAmount := transactions[i].TotalPrice
remainingToConsume := salesAmount
// Consume from oldest allocations first (FIFO)
for j := range allocations {
if remainingToConsume <= 0 {
break
}
available := allocations[j].amount - allocations[j].consumed
if available > 0 {
consume := available
if consume > remainingToConsume {
consume = remainingToConsume
}
allocations[j].consumed += consume
remainingToConsume -= consume
}
}
runningBalance -= salesAmount
}
}
// Now find which allocation covers the current sales
amountNeeded := currentSales.TotalPrice
for _, alloc := range allocations {
available := alloc.amount - alloc.consumed
if available > 0 {
if amountNeeded <= available {
// This allocation fully covers the sales
return "LUNAS", &alloc.date
} else {
// This allocation partially covers, continue to next
amountNeeded -= available
}
}
}
// If we get here, use the oldest allocation
if len(allocations) > 0 {
return "LUNAS", &allocations[0].date
}
return "LUNAS", nil return "LUNAS", nil
} }
@@ -1370,15 +1426,17 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
} }
for pfkID, egg := range eggMap { for pfkID, egg := range eggMap {
if rowIdx, ok := pfkIndex[pfkID]; ok { if rowIdx, ok := pfkIndex[pfkID]; ok {
repoRows[rowIdx].EggProductionWeightKg = egg.EggProductionWeightKg repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
repoRows[rowIdx].EggProductionPieces = egg.EggProductionPieces repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
} }
} }
} }
costMap := make(map[uint]HppCostAggregate, len(costRows)) costMap := make(map[uint]HppCostAggregate, len(costRows))
for _, row := range costRows { for _, row := range costRows {
costMap[row.KandangID] = HppCostAggregate{ costMap[row.ProjectFlockKandangID] = HppCostAggregate{
FeedCost: row.FeedCost, FeedCost: row.FeedCost,
OvkCost: row.OvkCost, OvkCost: row.OvkCost,
DocCost: row.DocCost, DocCost: row.DocCost,
@@ -1407,15 +1465,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
category = "DOC" category = "DOC"
} }
if seen[sup.KandangID] == nil { if seen[sup.ProjectFlockKandangID] == nil {
seen[sup.KandangID] = make(map[uint]bool) seen[sup.ProjectFlockKandangID] = make(map[uint]bool)
} }
if seen[sup.KandangID][sup.SupplierID] { if seen[sup.ProjectFlockKandangID][sup.SupplierID] {
continue continue
} }
seen[sup.KandangID][sup.SupplierID] = true seen[sup.ProjectFlockKandangID][sup.SupplierID] = true
targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{ targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], dto.HppPerKandangSupplierDTO{
ID: int64(sup.SupplierID), ID: int64(sup.SupplierID),
Name: sup.SupplierName, Name: sup.SupplierName,
Alias: sup.SupplierAlias, Alias: sup.SupplierAlias,
@@ -1442,12 +1500,12 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows)) dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate) perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
var totalBirds int64 var totalBirds int64
var totalWeight float64 // var totalWeight float64
var totalEggPieces int64 var totalEggPieces int64
var totalEggKg float64 var totalEggKg float64
var totalRemainingValueRp int64 // var totalRemainingValueRp int64
var totalEggValueRp int64 var totalEggValueRp int64
var totalHppSum float64 // var totalHppSum float64
var totalHppCount int var totalHppCount int
var totalDocPriceSum float64 var totalDocPriceSum float64
var totalDocPriceCount int var totalDocPriceCount int
@@ -1461,26 +1519,34 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
continue continue
} }
birdsFloat := row.RemainingChickenBirds // birdsFloat := row.RemainingChickenBirds
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) { // if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
birdsFloat = 0 // birdsFloat = 0
// }
// weightFloat := row.RemainingChickenWeight
// if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
// weightFloat = 0
// }
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
eggPiecesFloatRemaining = 0
} }
weightFloat := row.RemainingChickenWeight eggTotalPiecesFloat := row.EggProductionTotalPieces
if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) { if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
weightFloat = 0 eggTotalPiecesFloat = 0
} }
eggPiecesFloat := row.EggProductionPieces eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) { if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
eggPiecesFloat = 0 eggRemainingWeightFloatRemaining = 0
} }
eggWeightFloat := row.EggProductionWeightKg eggWeightFloat := row.EggProductionTotalWeightKg
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) { if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
eggWeightFloat = 0 eggWeightFloat = 0
} }
avgWeight := 0.0 avgWeight := 0.0
if eggPiecesFloat > 0 { if eggTotalPiecesFloat > 0 {
avgWeight = eggWeightFloat / eggPiecesFloat avgWeight = eggWeightFloat / eggTotalPiecesFloat
} }
if params.WeightMin != nil && avgWeight < *params.WeightMin { if params.WeightMin != nil && avgWeight < *params.WeightMin {
continue continue
@@ -1496,28 +1562,30 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
weightMax := weightMin + 0.09 weightMax := weightMin + 0.09
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax} rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
rowBirds := int64(math.Round(birdsFloat)) // rowBirds := int64(math.Round(birdsFloat))
costEntry := costMap[row.KandangID] costEntry := costMap[row.ProjectFlockKandangID]
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
hppRp := 0.0 // hppRp := 0.0
if weightFloat > 0 { // if weightFloat > 0 {
hppRp = totalCost / weightFloat // hppRp = totalCost / weightFloat
} // }
eggHpp := 0.0 eggHpp := 0.0
if eggWeightFloat > 0 { if eggWeightFloat > 0 {
eggHpp = totalCost / eggWeightFloat eggHpp = (totalCost / eggWeightFloat) / 1000
} }
rowEggPieces := int64(math.Round(eggPiecesFloat)) rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
rowEggValue := int64(eggHpp * eggWeightFloat) rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
rowRemainingValue := int64(hppRp * weightFloat) // rowRemainingValue := int64(hppRp * weightFloat)
avgDocPrice := int64(0) avgDocPrice := int64(0)
if costEntry.DocQty > 0 { if costEntry.DocQty > 0 {
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty)) avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
} }
nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)
dataRows = append(dataRows, dto.HppPerKandangRowDTO{ dataRows = append(dataRows, dto.HppPerKandangRowDTO{
ID: int(row.KandangID), ID: int(row.ProjectFlockKandangID),
Kandang: dto.HppPerKandangRowKandangDTO{ Kandang: dto.HppPerKandangRowKandangDTO{
ID: int64(row.KandangID), ID: int64(row.KandangID),
Name: row.KandangName, Name: row.KandangName,
@@ -1535,32 +1603,35 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
WeightMin: weightMin, WeightMin: weightMin,
WeightMax: weightMax, WeightMax: weightMax,
}, },
AvgWeightKg: avgWeight, AvgWeightKg: avgWeight,
NameWithPeriode: nameWithPeriod,
// FeedCostRp: costEntry.FeedCost, // FeedCostRp: costEntry.FeedCost,
// OvkCostRp: costEntry.OvkCost, // OvkCostRp: costEntry.OvkCost,
DocSuppliers: docSupplierMap[row.KandangID], DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
FeedSuppliers: feedSupplierMap[row.KandangID], FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
EggProductionPieces: rowEggPieces, EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
EggProductionKg: eggWeightFloat, EggProductionKg: eggRemainingWeightFloatRemaining,
AverageDocPriceRp: avgDocPrice, // EggProductionTotalWeightKg: eggWeightFloat,
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)),
AverageDocPriceRp: avgDocPrice,
// HppRp: hppRp, // HppRp: hppRp,
EggHppRpPerKg: eggHpp, EggHppRpPerKg: eggHpp,
RemainingValueRp: rowRemainingValue, // RemainingValueRp: rowRemainingValue,
EggValueRp: rowEggValue, EggValueRp: rowEggValue,
}) })
totalBirds += rowBirds // totalBirds += rowBirds
totalWeight += weightFloat // totalWeight += weightFloat
totalEggPieces += rowEggPieces totalEggPieces += rowEggPieces
totalEggKg += eggWeightFloat totalEggKg += eggRemainingWeightFloatRemaining
totalRemainingValueRp += rowRemainingValue // totalRemainingValueRp += rowRemainingValue
totalEggValueRp += rowEggValue totalEggValueRp += rowEggValue
totalAvgWeightSum += avgWeight totalAvgWeightSum += avgWeight
totalAvgWeightCount++ totalAvgWeightCount++
if weightFloat > 0 { // if weightFloat > 0 {
totalHppSum += hppRp // totalHppSum += hppRp
totalHppCount++ // totalHppCount++
} // }
if avgDocPrice > 0 { if avgDocPrice > 0 {
totalDocPriceSum += float64(avgDocPrice) totalDocPriceSum += float64(avgDocPrice)
totalDocPriceCount++ totalDocPriceCount++
@@ -1587,23 +1658,23 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
} }
rangeSummary := rangeAgg.Summary rangeSummary := rangeAgg.Summary
rangeAgg.RemainingBirds += rowBirds // rangeAgg.RemainingBirds += rowBirds
rangeAgg.RemainingWeightKg += row.RemainingChickenWeight // rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
rangeAgg.AvgWeightSum += avgWeight rangeAgg.AvgWeightSum += avgWeight
rangeAgg.AvgWeightCount++ rangeAgg.AvgWeightCount++
for _, supplier := range feedSupplierMap[row.KandangID] { for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok { if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
rangeAgg.FeedSuppliers[supplier.ID] = supplier rangeAgg.FeedSuppliers[supplier.ID] = supplier
} }
} }
for _, supplier := range docSupplierMap[row.KandangID] { for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok { if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
rangeAgg.DocSuppliers[supplier.ID] = supplier rangeAgg.DocSuppliers[supplier.ID] = supplier
} }
} }
rangeSummary.EggProductionPieces += rowEggPieces rangeSummary.EggProductionPieces += rowEggPieces
rangeSummary.EggProductionKg += eggWeightFloat rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
rangeSummary.RemainingValueRp += rowRemainingValue // rangeSummary.RemainingValueRp += rowRemainingValue
rangeSummary.EggValueRp += rowEggValue rangeSummary.EggValueRp += rowEggValue
if eggWeightFloat > 0 { if eggWeightFloat > 0 {
rangeAgg.EggHppSum += eggHpp rangeAgg.EggHppSum += eggHpp
@@ -1750,6 +1821,9 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
if err != nil { if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
if weightMin != nil && weightMax != nil && *weightMin > *weightMax {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max")
}
params := &validation.HppPerKandangQuery{ params := &validation.HppPerKandangQuery{
Page: page, Page: page,
@@ -17,12 +17,14 @@ type ExpenseQuery struct {
type MarketingQuery struct { type MarketingQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"` Search string `query:"search" validate:"omitempty,max=100"`
CustomerId int64 `query:"customer_id" validate:"omitempty"` CustomerId int64 `query:"customer_id" validate:"omitempty"`
ProductId int64 `query:"product_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"`
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
AreaId int64 `query:"area_id" validate:"omitempty"`
LocationId int64 `query:"location_id" validate:"omitempty"`
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"` MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"` FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
@@ -56,7 +58,7 @@ type DebtSupplierQuery struct {
type HppPerKandangQuery struct { type HppPerKandangQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
Period string `query:"period" validate:"required"` Period string `query:"period" validate:"required"`
ShowUnrecorded bool `query:"show_unrecorded"` ShowUnrecorded bool `query:"show_unrecorded"`
AreaIDs []int64 `query:"-"` AreaIDs []int64 `query:"-"`
@@ -68,7 +70,7 @@ type HppPerKandangQuery struct {
type ProductionResultQuery struct { type ProductionResultQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"` ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
} }