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"`
SourceProjectFlockKandangId uint `gorm:"not null"`
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
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
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 warehouses w ON w.id = pw.warehouse_id
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
}
if req.Notes != nil {
updateBody["notes"] = *req.Notes
}
if req.LocationID != nil {
locationID := uint(*req.LocationID)
updateBody["location_id"] = locationID
@@ -568,20 +572,28 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err != nil {
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
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
if previousStep < utils.ExpenseStepPengajuan {
previousStep = utils.ExpenseStepPengajuan
}
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowExpense,
id,
utils.ExpenseStepPengajuan,
previousStep,
&approvalAction,
actorID,
nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
}
}
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"`
SupplierID *uint64 `form:"supplier_id" json:"supplier_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"`
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
}
@@ -4,20 +4,17 @@ import (
"time"
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"
)
type TransferRelationDTO struct {
Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
}
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Id uint64 `json:"id"`
MovementNumber string `json:"movement_number"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
}
type ProductSimpleDTO struct {
@@ -25,16 +22,6 @@ type ProductSimpleDTO struct {
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 {
Id uint `json:"id"`
Name string `json:"name"`
@@ -48,13 +35,6 @@ type DocumentDTO struct {
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 {
TransferRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
@@ -97,16 +77,19 @@ type TransferDeliveryItemDTO struct {
}
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
var sourceWarehouse *WarehouseDetailDTO
var sourceWarehouse *warehouseDTO.WarehouseRelationDTO
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 {
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse)
destinationWarehouse = &mapped
}
return TransferRelationDTO{
Id: e.Id,
MovementNumber: e.MovementNumber,
TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"),
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 {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil {
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
route := v1.Group("/transfers")
route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
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 {
db = s.withRelations(db)
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")
})
@@ -118,9 +122,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
})
if err != nil {
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
@@ -136,12 +140,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
)
if err != nil {
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 {
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)
}
@@ -161,10 +165,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
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 {
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)
@@ -192,16 +196,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
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.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) {
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())
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)
@@ -239,16 +243,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
)
if err != nil {
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(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
)
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) {
ctx := c.Context()
@@ -256,14 +260,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if err != nil {
return err
}
// Set ProjectFlockKandangId hanya jika ada kandang
var pfkID *uint
if projectFlockKandangID > 0 {
pfkID = &projectFlockKandangID
}
destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0,
ProjectFlockKandangId: &projectFlockKandangID,
ProjectFlockKandangId: pfkID,
}
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 {
detail, ok := detailMap[uint64(prod.ProductID)]
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{
StockTransferDeliveryId: delivery.Id,
@@ -372,7 +383,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx,
})
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{}).
@@ -381,7 +392,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
"usage_qty": consumeResult.UsageQuantity,
"pending_qty": consumeResult.PendingQuantity,
}).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)
@@ -394,7 +405,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Tx: tx,
})
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{}).
@@ -402,7 +413,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Updates(map[string]interface{}{
"total_qty": replenishResult.AddedQuantity,
}).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 {
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))
@@ -446,8 +457,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if len(expensePayloads) > 0 {
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("Failed to sync expense: %v", err))
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber))
}
}
@@ -461,32 +471,26 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
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) {
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
if err != nil {
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.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 {
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))
if err != nil {
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
@@ -140,12 +140,21 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
if actorID == 0 {
actorID = 1
}
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
action := entity.ApprovalActionUpdated
approvalRepo := commonRepo.NewApprovalRepository(b.db)
svc := commonSvc.NewApprovalService(approvalRepo)
action := entity.ApprovalActionCreated
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
}
if latestApproval == nil {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
}
return nil
}
@@ -140,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
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")
}
@@ -148,18 +148,30 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
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")
}
if filters.Search != "" {
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 + "%"
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
searchPattern, searchPattern, searchPattern, searchPattern)
db = db.Where(`(
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 {
@@ -178,6 +190,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
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 != "" {
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Group("marketing_delivery_products.id")
@@ -186,7 +211,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
case "ayam":
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
})
case "telur":
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)
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId)
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
@@ -1,6 +1,7 @@
package dto
import (
"strconv"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -53,6 +54,7 @@ type ProjectFlockKandangListDTO struct {
ProjectFlockKandangRelationDTO
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
NameWithPeriod string `json:"name_with_period"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
@@ -104,6 +106,7 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
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
}
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(
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
approval := selector(e)
@@ -147,6 +160,7 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
Kandang: toKandangRelation(e.Kandang),
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
CreatedAt: e.CreatedAt,
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
@@ -2,6 +2,7 @@ package repository
import (
"context"
"math"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -16,6 +17,7 @@ type ProjectFlockPopulationRepository interface {
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID 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
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).
Model(&entity.ProjectFlockPopulation{}).
Where("product_warehouse_id = ?", productWarehouseID).
Select("COALESCE(SUM(total_qty), 0)").
Select("COALESCE(SUM(total_qty - total_used_qty), 0)").
Scan(&total).Error
if err != nil {
return 0, err
@@ -135,3 +137,22 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand
}
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 {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Page: c.QueryInt("page", 1),
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 {
@@ -179,7 +183,6 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
})
}
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
if err != nil {
@@ -162,9 +162,19 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
}
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{
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
Qty: displayQty,
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
Note: source.Note,
}
@@ -110,8 +110,31 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
offset := (params.Page - 1) * params.Limit
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 = s.withRelations(db)
return db
})
@@ -216,7 +239,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, sourceDetail := range req.SourceKandangs {
if sourceDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
continue
}
totalSourceQty += sourceDetail.Quantity
@@ -247,11 +270,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
continue
}
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 {
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 {
if sourceDetail.Quantity == 0 {
continue
}
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
source := entity.LayingTransferSource{
LayingTransferId: createBody.Id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
UsageQty: 0,
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
ProductWarehouseId: &productWarehouseId,
@@ -295,6 +330,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
}
for _, targetDetail := range req.TargetKandangs {
if targetDetail.Quantity == 0 {
continue
}
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
if err != nil {
@@ -463,8 +501,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
source := entity.LayingTransferSource{
LayingTransferId: id,
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
UsageQty: 0,
PendingUsageQty: sourceDetail.Quantity,
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
ProductWarehouseId: &productWarehouseId,
}
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))
}
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{
StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id,
@@ -814,15 +853,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
kandangAvailableQty := make(map[uint]float64)
for _, kandang := range kandangs {
totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
// Gunakan fungsi repository yang sama dengan recording service
totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
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
continue
}
kandangAvailableQty[kandang.Id] = totalQty
kandangAvailableQty[kandang.Id] = totalAvailable
}
return pf, kandangAvailableQty, nil
@@ -2,12 +2,12 @@ package validation
type SourceKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
Quantity float64 `json:"quantity"`
}
type TargetKandangDetail struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
Quantity float64 `json:"quantity"`
}
type Create struct {
@@ -29,8 +29,12 @@ type Update struct {
}
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"`
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"`
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 {
@@ -167,12 +167,21 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[
if actorID == 0 {
actorID = 1
}
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
action := entity.ApprovalActionUpdated
approvalRepo := commonRepo.NewApprovalRepository(b.db)
svc := commonSvc.NewApprovalService(approvalRepo)
action := entity.ApprovalActionCreated
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
}
if latestApproval == nil {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
}
return nil
}
@@ -82,6 +82,8 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
ProductId: int64(ctx.QueryInt("product_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_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", ""),
FilterBy: ctx.Query("filter_by", ""),
StartDate: ctx.Query("start_date", ""),
@@ -27,10 +27,13 @@ type HppPerKandangResponseData struct {
type HppPerKandangRowDTO struct {
ID int `json:"id"`
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
NameWithPeriode string `json:"name_with_periode"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"`
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"`
// OvkCostRp float64 `json:"ovk_cost_rp"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
@@ -38,8 +41,8 @@ type HppPerKandangRowDTO struct {
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"`
// HppRp float64 `json:"hpp_rp"`
// RemainingValueRp int64 `json:"remaining_value_rp"`
}
type HppPerKandangRowKandangDTO struct {
@@ -11,37 +11,40 @@ import (
)
type HppPerKandangRow struct {
ProjectFlockKandangID uint
KandangID uint
KandangName string
KandangStatus string
LocationID uint
LocationName string
PicID uint
PicName string
RecordingCount int64
RemainingChickenBirds float64
RemainingChickenWeight float64
EggProductionWeightKg float64
EggProductionPieces float64
ProjectFlockKandangID uint
ProjectFlockPeriod int
KandangID uint
KandangName string
KandangStatus string
LocationID uint
LocationName string
PicID uint
PicName string
RecordingCount int64
// RemainingChickenBirds float64
// RemainingChickenWeight float64
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
}
type HppPerKandangCostRow struct {
KandangID uint
FeedCost float64
OvkCost float64
DocCost float64
DocQty float64
BudgetCost float64
ExpenseCost float64
ProjectFlockKandangID uint
FeedCost float64
OvkCost float64
DocCost float64
DocQty float64
BudgetCost float64
ExpenseCost float64
}
type HppPerKandangSupplierRow struct {
KandangID uint
SupplierID uint
SupplierName string
SupplierAlias string
Category string
ProjectFlockKandangID uint
SupplierID uint
SupplierName string
SupplierAlias string
Category string
}
type HppPerKandangRepository interface {
@@ -86,6 +89,7 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
Table("project_flocks AS pf").
Select(`
pfk.id AS project_flock_kandang_id,
pfk.period AS project_flock_period,
k.id AS kandang_id,
k.name AS kandang_name,
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,
0 AS remaining_chicken_weight,
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(`
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 locations AS loc ON loc.id = k.location_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).
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 = query.Group("pfk.id, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
Order("k.id ASC")
query = query.Group("pfk.id, pfk.period, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
Order("pfk.id ASC")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
@@ -139,7 +152,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
k.id AS kandang_id,
pfk.id AS project_flock_kandang_id,
COALESCE(SUM(CASE
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)
@@ -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 tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
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("(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 {
return nil, nil, err
}
docRows := make([]struct {
KandangID uint
DocCost float64
DocQty float64
SupplierID *uint
SupplierName *string
SupplierAlias *string
ProjectFlockKandangID uint
DocCost float64
DocQty float64
SupplierID *uint
SupplierName *string
SupplierAlias *string
}, 0)
docQuery := r.db.WithContext(ctx).
Table("project_chickins AS pc").
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), 0) AS doc_qty,
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 suppliers AS s ON s.id = pur.supplier_id").
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 {
return nil, nil, err
@@ -208,28 +221,28 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
for i := range rows {
row := rows[i]
costMap[row.KandangID] = &rows[i]
costMap[row.ProjectFlockKandangID] = &rows[i]
}
docSuppliers := make([]HppPerKandangSupplierRow, 0)
docSeen := make(map[uint]map[uint]bool)
for _, doc := range docRows {
entry, ok := costMap[doc.KandangID]
entry, ok := costMap[doc.ProjectFlockKandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
KandangID: doc.KandangID,
ProjectFlockKandangID: doc.ProjectFlockKandangID,
})
entry = &rows[len(rows)-1]
costMap[doc.KandangID] = entry
costMap[doc.ProjectFlockKandangID] = entry
}
entry.DocCost += doc.DocCost
entry.DocQty += doc.DocQty
if doc.SupplierID != nil {
if docSeen[doc.KandangID] == nil {
docSeen[doc.KandangID] = make(map[uint]bool)
if docSeen[doc.ProjectFlockKandangID] == nil {
docSeen[doc.ProjectFlockKandangID] = make(map[uint]bool)
}
if !docSeen[doc.KandangID][*doc.SupplierID] {
docSeen[doc.KandangID][*doc.SupplierID] = true
if !docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] {
docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] = true
supplierName := ""
if doc.SupplierName != nil {
supplierName = *doc.SupplierName
@@ -239,19 +252,19 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
supplierAlias = *doc.SupplierAlias
}
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
KandangID: doc.KandangID,
SupplierID: *doc.SupplierID,
SupplierName: supplierName,
SupplierAlias: supplierAlias,
Category: "DOC",
ProjectFlockKandangID: doc.ProjectFlockKandangID,
SupplierID: *doc.SupplierID,
SupplierName: supplierName,
SupplierAlias: supplierAlias,
Category: "DOC",
})
}
}
}
budgetRows := make([]struct {
KandangID uint
BudgetCost float64
ProjectFlockKandangID uint
BudgetCost float64
}, 0)
pfkUsageSub := r.db.
@@ -272,7 +285,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
budgetQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
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`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_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 p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("k.id")
Group("pfk.id")
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
@@ -288,33 +301,33 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
}
for _, budget := range budgetRows {
entry, ok := costMap[budget.KandangID]
entry, ok := costMap[budget.ProjectFlockKandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
KandangID: budget.KandangID,
ProjectFlockKandangID: budget.ProjectFlockKandangID,
})
entry = &rows[len(rows)-1]
costMap[budget.KandangID] = entry
costMap[budget.ProjectFlockKandangID] = entry
}
entry.BudgetCost += budget.BudgetCost
}
expenseRows := make([]struct {
KandangID uint
ExpenseCost float64
ProjectFlockKandangID uint
ExpenseCost float64
}, 0)
expenseQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
Select(`
k.id AS kandang_id,
pfk.id AS project_flock_kandang_id,
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_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_realizations AS er ON er.expense_nonstock_id = en.id").
Where("pfk.id IN (?)", projectFlockKandangIDs).
Group("k.id")
Group("pfk.id")
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
@@ -322,13 +335,13 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
}
for _, exp := range expenseRows {
entry, ok := costMap[exp.KandangID]
entry, ok := costMap[exp.ProjectFlockKandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
KandangID: exp.KandangID,
ProjectFlockKandangID: exp.ProjectFlockKandangID,
})
entry = &rows[len(rows)-1]
costMap[exp.KandangID] = entry
costMap[exp.ProjectFlockKandangID] = entry
}
entry.ExpenseCost += exp.ExpenseCost
}
@@ -337,7 +350,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
feedQuery := r.db.WithContext(ctx).
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 kandangs AS k ON k.id = pfk.kandang_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).
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
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")
// feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
@@ -358,11 +371,11 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
}
for i := range feedSuppliers {
if _, exists := costMap[feedSuppliers[i].KandangID]; !exists {
if _, exists := costMap[feedSuppliers[i].ProjectFlockKandangID]; !exists {
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"
}
@@ -391,9 +404,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
)
type eggRow struct {
ProjectFlockKandangID uint
EggProductionWeightKg float64
EggProductionPieces float64
ProjectFlockKandangID uint
EggProductionWeightKgRemaining float64
EggProductionPiecesRemaining float64
EggProductionTotalWeightKg float64
EggProductionTotalPieces float64
}
eggRows := make([]eggRow, 0)
@@ -401,12 +416,14 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
Table("recordings AS r").
Select(`
r.project_flock_kandangs_id AS project_flock_kandang_id,
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
COALESCE(SUM((re.total_qty - re.total_used) * re.weight / 1000), 0) AS egg_production_weight_kg_remaining,
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 recording_eggs AS re ON re.recording_id = r.id").
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("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Group("r.project_flock_kandangs_id")
@@ -418,9 +435,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
result := make(map[uint]HppPerKandangRow, len(eggRows))
for _, row := range eggRows {
result[row.ProjectFlockKandangID] = HppPerKandangRow{
ProjectFlockKandangID: row.ProjectFlockKandangID,
EggProductionWeightKg: row.EggProductionWeightKg,
EggProductionPieces: row.EggProductionPieces,
ProjectFlockKandangID: row.ProjectFlockKandangID,
EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
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)
}
if len(kandangIDs) > 0 {
query = query.Where("k.id IN ?", kandangIDs)
query = query.Where("pfk.id IN ?", kandangIDs)
}
return query
}
@@ -464,9 +464,13 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
if err != nil {
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
}
@@ -503,14 +507,8 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
row.Status = status
if status == "LUNAS" {
if previousBalance >= tx.TotalPrice {
days := 0
row.AgingDay = &days
} else if paymentDate != nil {
if paymentDate != nil {
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
} else {
days := 0
@@ -518,9 +516,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
}
} else {
days := int(time.Since(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
}
} else if tx.TransactionType == "PAYMENT" {
@@ -586,6 +581,67 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
// 4. BELUM LUNAS: no payment at all
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
}
@@ -1370,15 +1426,17 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
}
for pfkID, egg := range eggMap {
if rowIdx, ok := pfkIndex[pfkID]; ok {
repoRows[rowIdx].EggProductionWeightKg = egg.EggProductionWeightKg
repoRows[rowIdx].EggProductionPieces = egg.EggProductionPieces
repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
}
}
}
costMap := make(map[uint]HppCostAggregate, len(costRows))
for _, row := range costRows {
costMap[row.KandangID] = HppCostAggregate{
costMap[row.ProjectFlockKandangID] = HppCostAggregate{
FeedCost: row.FeedCost,
OvkCost: row.OvkCost,
DocCost: row.DocCost,
@@ -1407,15 +1465,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
category = "DOC"
}
if seen[sup.KandangID] == nil {
seen[sup.KandangID] = make(map[uint]bool)
if seen[sup.ProjectFlockKandangID] == nil {
seen[sup.ProjectFlockKandangID] = make(map[uint]bool)
}
if seen[sup.KandangID][sup.SupplierID] {
if seen[sup.ProjectFlockKandangID][sup.SupplierID] {
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),
Name: sup.SupplierName,
Alias: sup.SupplierAlias,
@@ -1442,12 +1500,12 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
var totalBirds int64
var totalWeight float64
// var totalWeight float64
var totalEggPieces int64
var totalEggKg float64
var totalRemainingValueRp int64
// var totalRemainingValueRp int64
var totalEggValueRp int64
var totalHppSum float64
// var totalHppSum float64
var totalHppCount int
var totalDocPriceSum float64
var totalDocPriceCount int
@@ -1461,26 +1519,34 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
continue
}
birdsFloat := row.RemainingChickenBirds
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
birdsFloat = 0
// birdsFloat := row.RemainingChickenBirds
// if math.IsNaN(birdsFloat) || math.IsInf(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
if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
weightFloat = 0
eggTotalPiecesFloat := row.EggProductionTotalPieces
if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
eggTotalPiecesFloat = 0
}
eggPiecesFloat := row.EggProductionPieces
if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) {
eggPiecesFloat = 0
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
eggRemainingWeightFloatRemaining = 0
}
eggWeightFloat := row.EggProductionWeightKg
eggWeightFloat := row.EggProductionTotalWeightKg
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
eggWeightFloat = 0
}
avgWeight := 0.0
if eggPiecesFloat > 0 {
avgWeight = eggWeightFloat / eggPiecesFloat
if eggTotalPiecesFloat > 0 {
avgWeight = eggWeightFloat / eggTotalPiecesFloat
}
if params.WeightMin != nil && avgWeight < *params.WeightMin {
continue
@@ -1496,28 +1562,30 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
weightMax := weightMin + 0.09
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
rowBirds := int64(math.Round(birdsFloat))
costEntry := costMap[row.KandangID]
// rowBirds := int64(math.Round(birdsFloat))
costEntry := costMap[row.ProjectFlockKandangID]
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
hppRp := 0.0
if weightFloat > 0 {
hppRp = totalCost / weightFloat
}
// hppRp := 0.0
// if weightFloat > 0 {
// hppRp = totalCost / weightFloat
// }
eggHpp := 0.0
if eggWeightFloat > 0 {
eggHpp = totalCost / eggWeightFloat
eggHpp = (totalCost / eggWeightFloat) / 1000
}
rowEggPieces := int64(math.Round(eggPiecesFloat))
rowEggValue := int64(eggHpp * eggWeightFloat)
rowRemainingValue := int64(hppRp * weightFloat)
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
// rowRemainingValue := int64(hppRp * weightFloat)
avgDocPrice := int64(0)
if costEntry.DocQty > 0 {
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
}
nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
ID: int(row.KandangID),
ID: int(row.ProjectFlockKandangID),
Kandang: dto.HppPerKandangRowKandangDTO{
ID: int64(row.KandangID),
Name: row.KandangName,
@@ -1535,32 +1603,35 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
WeightMin: weightMin,
WeightMax: weightMax,
},
AvgWeightKg: avgWeight,
AvgWeightKg: avgWeight,
NameWithPeriode: nameWithPeriod,
// FeedCostRp: costEntry.FeedCost,
// OvkCostRp: costEntry.OvkCost,
DocSuppliers: docSupplierMap[row.KandangID],
FeedSuppliers: feedSupplierMap[row.KandangID],
EggProductionPieces: rowEggPieces,
EggProductionKg: eggWeightFloat,
AverageDocPriceRp: avgDocPrice,
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
EggProductionKg: eggRemainingWeightFloatRemaining,
// EggProductionTotalWeightKg: eggWeightFloat,
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)),
AverageDocPriceRp: avgDocPrice,
// HppRp: hppRp,
EggHppRpPerKg: eggHpp,
RemainingValueRp: rowRemainingValue,
EggValueRp: rowEggValue,
EggHppRpPerKg: eggHpp,
// RemainingValueRp: rowRemainingValue,
EggValueRp: rowEggValue,
})
totalBirds += rowBirds
totalWeight += weightFloat
// totalBirds += rowBirds
// totalWeight += weightFloat
totalEggPieces += rowEggPieces
totalEggKg += eggWeightFloat
totalRemainingValueRp += rowRemainingValue
totalEggKg += eggRemainingWeightFloatRemaining
// totalRemainingValueRp += rowRemainingValue
totalEggValueRp += rowEggValue
totalAvgWeightSum += avgWeight
totalAvgWeightCount++
if weightFloat > 0 {
totalHppSum += hppRp
totalHppCount++
}
// if weightFloat > 0 {
// totalHppSum += hppRp
// totalHppCount++
// }
if avgDocPrice > 0 {
totalDocPriceSum += float64(avgDocPrice)
totalDocPriceCount++
@@ -1587,23 +1658,23 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
}
rangeSummary := rangeAgg.Summary
rangeAgg.RemainingBirds += rowBirds
rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
// rangeAgg.RemainingBirds += rowBirds
// rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
rangeAgg.AvgWeightSum += avgWeight
rangeAgg.AvgWeightCount++
for _, supplier := range feedSupplierMap[row.KandangID] {
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
rangeAgg.FeedSuppliers[supplier.ID] = supplier
}
}
for _, supplier := range docSupplierMap[row.KandangID] {
for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
rangeAgg.DocSuppliers[supplier.ID] = supplier
}
}
rangeSummary.EggProductionPieces += rowEggPieces
rangeSummary.EggProductionKg += eggWeightFloat
rangeSummary.RemainingValueRp += rowRemainingValue
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
// rangeSummary.RemainingValueRp += rowRemainingValue
rangeSummary.EggValueRp += rowEggValue
if eggWeightFloat > 0 {
rangeAgg.EggHppSum += eggHpp
@@ -1750,6 +1821,9 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
if err != nil {
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{
Page: page,
@@ -17,12 +17,14 @@ type ExpenseQuery struct {
type MarketingQuery struct {
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"`
CustomerId int64 `query:"customer_id" validate:"omitempty"`
ProductId int64 `query:"product_id" validate:"omitempty"`
WarehouseId int64 `query:"warehouse_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"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
@@ -56,7 +58,7 @@ type DebtSupplierQuery struct {
type HppPerKandangQuery struct {
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"`
ShowUnrecorded bool `query:"show_unrecorded"`
AreaIDs []int64 `query:"-"`
@@ -68,7 +70,7 @@ type HppPerKandangQuery struct {
type ProductionResultQuery struct {
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"`
}