From 9fb5395469ae640a0a2ac943522360a45c090291 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 20 Jan 2026 10:13:58 +0700 Subject: [PATCH] [FIX/BE-US] recording,reporting,closing and uniformity --- ...650_add_fifo_recording_depletions.down.sql | 3 + ...95650_add_fifo_recording_depletions.up.sql | 17 ++ internal/entities/recording_depletion.go | 10 +- .../closings/dto/closingSapronak.dto.go | 46 ++++- .../repositories/closing.repository.go | 99 ++++++++++ .../closings/services/sapronak.service.go | 64 +++++- .../controllers/recording.controller.go | 5 +- .../recordings/dto/recording.dto.go | 95 ++++++--- .../modules/production/recordings/module.go | 22 +++ .../repositories/recording.repository.go | 70 +++++-- .../recordings/services/recording.service.go | 186 ++++++++++++++---- .../validations/recording.validation.go | 28 +-- .../services/uniformity.service.go | 78 +++++++- .../repports/dto/repportPurchase.dto.go | 32 ++- .../repositories/debt_supplier.repository.go | 82 +++++++- .../purchase_supplier.repository.go | 29 ++- .../repports/services/repport.service.go | 72 ++++++- internal/utils/fifo/constants.go | 23 +-- internal/utils/recording/util.recording.go | 5 - 19 files changed, 805 insertions(+), 161 deletions(-) create mode 100644 internal/database/migrations/20260119095650_add_fifo_recording_depletions.down.sql create mode 100644 internal/database/migrations/20260119095650_add_fifo_recording_depletions.up.sql diff --git a/internal/database/migrations/20260119095650_add_fifo_recording_depletions.down.sql b/internal/database/migrations/20260119095650_add_fifo_recording_depletions.down.sql new file mode 100644 index 00000000..4daff87b --- /dev/null +++ b/internal/database/migrations/20260119095650_add_fifo_recording_depletions.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE recording_depletions + DROP COLUMN IF EXISTS pending_qty, + DROP COLUMN IF EXISTS source_product_warehouse_id; diff --git a/internal/database/migrations/20260119095650_add_fifo_recording_depletions.up.sql b/internal/database/migrations/20260119095650_add_fifo_recording_depletions.up.sql new file mode 100644 index 00000000..4e29e129 --- /dev/null +++ b/internal/database/migrations/20260119095650_add_fifo_recording_depletions.up.sql @@ -0,0 +1,17 @@ +ALTER TABLE recording_depletions + ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint; + +UPDATE recording_depletions rd +SET source_product_warehouse_id = src.product_warehouse_id +FROM recordings r +JOIN LATERAL ( + SELECT pfp.product_warehouse_id + FROM project_chickins pc + JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id + WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id + ORDER BY pfp.created_at ASC, pfp.id ASC + LIMIT 1 +) AS src ON true +WHERE r.id = rd.recording_id + AND rd.source_product_warehouse_id IS NULL; diff --git a/internal/entities/recording_depletion.go b/internal/entities/recording_depletion.go index 53af300d..8e0c7afe 100644 --- a/internal/entities/recording_depletion.go +++ b/internal/entities/recording_depletion.go @@ -1,10 +1,12 @@ package entities type RecordingDepletion struct { - Id uint `gorm:"primaryKey"` - RecordingId uint `gorm:"column:recording_id;not null;index"` - ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` - Qty float64 `gorm:"column:qty;not null"` + Id uint `gorm:"primaryKey"` + RecordingId uint `gorm:"column:recording_id;not null;index"` + ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` + SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"` + Qty float64 `gorm:"column:qty;not null"` + PendingQty float64 `gorm:"column:pending_qty"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` diff --git a/internal/modules/closings/dto/closingSapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go index 768c727e..0067b9d2 100644 --- a/internal/modules/closings/dto/closingSapronak.dto.go +++ b/internal/modules/closings/dto/closingSapronak.dto.go @@ -201,18 +201,48 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin switch strings.ToLower(item.JenisTransaksi) { case "pembelian", "adjustment masuk", "mutasi masuk": row.QtyIn += item.QtyMasuk - row.TotalAmount += item.Nilai + if row.UnitPrice == 0 { + if item.QtyMasuk > 0 && item.Nilai > 0 { + row.UnitPrice = item.Nilai / item.QtyMasuk + } else if item.Harga > 0 { + row.UnitPrice = item.Harga + } + } + if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" { + ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi)) + if strings.HasPrefix(ref, "TL-") { + row.Notes = "TRANSFER LAYING" + } else if strings.HasPrefix(ref, "ST-") { + row.Notes = "TRANSFER STOCK" + } + } case "pemakaian", "adjustment keluar": + price := row.UnitPrice + if price == 0 { + price = item.Harga + } row.QtyUsed += item.QtyKeluar - case "mutasi keluar": + row.TotalAmount += item.QtyKeluar * price + case "mutasi keluar", "penjualan": + price := row.UnitPrice + if price == 0 { + price = item.Harga + } row.QtyOut += item.QtyKeluar + if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" { + ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi)) + if strings.HasPrefix(ref, "TL-") { + row.Notes = "TRANSFER LAYING" + } else if strings.HasPrefix(ref, "ST-") { + row.Notes = "TRANSFER STOCK" + } + } default: row.QtyIn += item.QtyMasuk row.TotalAmount += item.Nilai - } - - if row.QtyIn > 0 { - row.UnitPrice = row.TotalAmount / row.QtyIn + if row.QtyIn > 0 { + row.UnitPrice = row.TotalAmount / row.QtyIn + } } } @@ -233,8 +263,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin total += r.TotalAmount } avg := 0.0 - if qtyIn > 0 { - avg = total / qtyIn + if qtyUsed > 0 { + avg = total / qtyUsed } cat.Total = SapronakCategoryTotalDTO{ Label: label, diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 507d7c88..6d7df1cc 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -33,6 +33,7 @@ type ClosingRepository interface { FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) + FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) } @@ -909,17 +910,50 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand COALESCE(p.product_price, 0) AS price `). Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). + Joins("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = std.product_id"). Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("w.kandang_id = ?", kandangID). + Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll) incoming, err := scanAndGroupDetails(incomingQuery) if err != nil { return nil, nil, err } + incomingLayingQuery := r.withCtx(ctx). + Table("laying_transfer_targets AS ltt"). + Select(` + pw.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + lt.transfer_date::timestamp AS date, + COALESCE(lt.transfer_number, '') AS reference, + COALESCE(ltt.total_qty, 0) AS qty_in, + 0 AS qty_out, + COALESCE(p.product_price, 0) AS price + `). + Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). + Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id"). + Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id"). + Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id"). + Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("JOIN products p ON p.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). + Where("w.kandang_id = ?", kandangID). + Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)"). + Where("f.name IN ?", sapronakFlagsAll) + incomingLaying, err := scanAndGroupDetails(incomingLayingQuery) + if err != nil { + return nil, nil, err + } + for pid, rows := range incomingLaying { + incoming[pid] = append(incoming[pid], rows...) + } + outgoingQuery := r.withCtx(ctx). Table("stock_allocations AS sa"). Select(` @@ -936,10 +970,13 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id"). + Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id"). Joins("JOIN products p ON p.id = std.product_id"). Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("w.kandang_id = ?", kandangID). + Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll). Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price") outgoing, err := scanAndGroupDetails(outgoingQuery) @@ -947,9 +984,71 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand return nil, nil, err } + outgoingLayingQuery := r.withCtx(ctx). + Table("stock_allocations AS sa"). + Select(` + pw.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + lt.transfer_date::timestamp AS date, + COALESCE(lt.transfer_number, '') AS reference, + 0 AS qty_in, + COALESCE(SUM(sa.qty), 0) AS qty_out, + COALESCE(p.product_price, 0) AS price + `). + Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()). + Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id"). + Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id"). + Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id"). + Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id"). + Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("JOIN products p ON p.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). + Where("sa.status = ?", entity.StockAllocationStatusActive). + Where("w.kandang_id = ?", kandangID). + Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). + Where("f.name IN ?", sapronakFlagsAll). + Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price") + outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery) + if err != nil { + return nil, nil, err + } + for pid, rows := range outgoingLaying { + outgoing[pid] = append(outgoing[pid], rows...) + } + return incoming, outgoing, nil } +func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) { + query := r.withCtx(ctx). + Table("stock_allocations AS sa"). + Select(` + pw.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + COALESCE(mdp.delivery_date, mdp.created_at) AS date, + COALESCE(m.so_number, '') AS reference, + 0 AS qty_in, + COALESCE(SUM(sa.qty), 0) AS qty_out, + COALESCE(mdp.unit_price, mp.unit_price, 0) AS price + `). + Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()). + Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). + Joins("JOIN marketings m ON m.id = mp.marketing_id"). + Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("JOIN products p ON p.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). + Where("sa.status = ?", entity.StockAllocationStatusActive). + Where("w.kandang_id = ?", kandangID). + Where("f.name IN ?", sapronakFlagsAll). + Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") + + return scanAndGroupDetails(query) +} + func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { if len(productIDs) == 0 { return []entity.Product{}, nil diff --git a/internal/modules/closings/services/sapronak.service.go b/internal/modules/closings/services/sapronak.service.go index fc354f46..930c1bc5 100644 --- a/internal/modules/closings/services/sapronak.service.go +++ b/internal/modules/closings/services/sapronak.service.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "strings" "github.com/go-playground/validator/v10" @@ -111,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val } // We no longer filter by date for closing sapronak report; pass nil pointers. - items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag) + items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag) if err != nil { s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report") @@ -262,6 +263,7 @@ type sapronakDetailMaps struct { AdjOutgoing map[uint][]dto.SapronakDetailDTO TransferIn map[uint][]dto.SapronakDetailDTO TransferOut map[uint][]dto.SapronakDetailDTO + SalesOut map[uint][]dto.SapronakDetailDTO } func buildSapronakDetails( @@ -271,6 +273,7 @@ func buildSapronakDetails( adjOutgoingRows map[uint][]repository.SapronakDetailRow, transferInRows map[uint][]repository.SapronakDetailRow, transferOutRows map[uint][]repository.SapronakDetailRow, + salesOutRows map[uint][]repository.SapronakDetailRow, ) sapronakDetailMaps { result := sapronakDetailMaps{ Incoming: make(map[uint][]dto.SapronakDetailDTO), @@ -279,6 +282,7 @@ func buildSapronakDetails( AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO), TransferIn: make(map[uint][]dto.SapronakDetailDTO), TransferOut: make(map[uint][]dto.SapronakDetailDTO), + SalesOut: make(map[uint][]dto.SapronakDetailDTO), } addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) { @@ -311,6 +315,7 @@ func buildSapronakDetails( addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false) addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true) addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false) + addRows(result.SalesOut, salesOutRows, "Penjualan", false) return result } @@ -350,6 +355,10 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj if err != nil { return nil, nil, 0, 0, err } + salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.KandangId) + if err != nil { + return nil, nil, 0, 0, err + } filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter)) matchesFlag := func(f string) bool { @@ -362,6 +371,34 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } return candidate == filterFlag } + dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO { + result := make(map[uint][]dto.SapronakDetailDTO, len(src)) + seen := make(map[string]struct{}) + for pid, rows := range src { + for _, d := range rows { + dateKey := "" + if d.Tanggal != nil { + dateKey = d.Tanggal.Format("2006-01-02") + } + qtyKey := d.QtyMasuk + if qtyKey == 0 { + qtyKey = d.QtyKeluar + } + + ref := strings.TrimSpace(d.NoReferensi) + key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey) + if ref == "" { + key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag))) + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + result[pid] = append(result[pid], d) + } + } + return result + } // For project flocks with category GROWING, pullet usage from chickin // should not be counted yet. Only when category is LAYING we allow @@ -400,13 +437,17 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...) } - detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows) + detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows) incomingDetails := detailMaps.Incoming usageDetails := detailMaps.Usage adjIncoming := detailMaps.AdjIncoming adjOutgoing := detailMaps.AdjOutgoing transIncoming := detailMaps.TransferIn transOutgoing := detailMaps.TransferOut + salesOutgoing := detailMaps.SalesOut + + transIncoming = dedupTransfers(transIncoming) + transOutgoing = dedupTransfers(transOutgoing) ensureGroup := func(flag string) *dto.SapronakGroupDTO { if g, ok := groupMap[flag]; ok { @@ -683,6 +724,25 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } } + for productID, details := range salesOutgoing { + flag, name := resolveFlagName(productID, details) + if !matchesFlag(flag) { + continue + } + group := ensureGroup(flag) + for _, d := range details { + if d.Flag == "" { + d.Flag = flag + } + if d.ProductName == "" { + d.ProductName = name + } + group.Items = append(group.Items, d) + group.TotalKeluar += d.QtyKeluar + group.SaldoAkhir -= d.QtyKeluar + } + } + groups := make([]dto.SapronakGroupDTO, 0, len(groupMap)) for _, g := range groupMap { groups = append(groups, *g) diff --git a/internal/modules/production/recordings/controllers/recording.controller.go b/internal/modules/production/recordings/controllers/recording.controller.go index c0f1737b..7edb7b9a 100644 --- a/internal/modules/production/recordings/controllers/recording.controller.go +++ b/internal/modules/production/recordings/controllers/recording.controller.go @@ -26,8 +26,9 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error { projectFlockID := c.QueryInt("project_flock_kandang_id", 0) 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"), } if projectFlockID > 0 { query.ProjectFlockKandangId = uint(projectFlockID) diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index ebb093ba..0fa14e97 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -15,13 +15,13 @@ import ( // === DTO Structs === type RecordingProjectFlockDTO struct { - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - FlockName string `json:"flock_name"` - ProjectFlockCategory string `json:"project_flock_category"` - Period int `json:"period"` - ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"` - Fcr *RecordingFcrDTO `json:"fcr,omitempty"` - TotalChickQty float64 `json:"total_chick_qty"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + FlockName string `json:"flock_name"` + ProjectFlockCategory string `json:"project_flock_category"` + Period int `json:"period"` + ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"` + Fcr *RecordingFcrDTO `json:"fcr,omitempty"` + TotalChickQty float64 `json:"total_chick_qty"` } type RecordingProductionStandardDTO struct { @@ -53,6 +53,13 @@ type RecordingLocationDTO struct { Address string `json:"address"` } +type RecordingKandangDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Capacity float64 `json:"capacity"` +} + type RecordingWarehouseDTO struct { Id uint `json:"id"` Name string `json:"name"` @@ -82,12 +89,14 @@ type RecordingListDTO struct { CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"` + Kandang *RecordingKandangDTO `json:"kandang,omitempty"` + Location *RecordingLocationDTO `json:"location,omitempty"` } type RecordingDetailDTO struct { RecordingListDTO - ProductCategory string `json:"product_category"` + ProductCategory string `json:"product_category"` + Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"` Depletions []RecordingDepletionDTO `json:"depletions"` Stocks []RecordingStockDTO `json:"stocks"` Eggs []RecordingEggDTO `json:"eggs"` @@ -133,10 +142,11 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { return RecordingDetailDTO{ RecordingListDTO: listDTO, - ProductCategory: recordingProductCategory(e), - Depletions: ToRecordingDepletionDTOs(e.Depletions), - Stocks: ToRecordingStockDTOs(e.Stocks), - Eggs: ToRecordingEggDTOs(e.Eggs), + ProductCategory: recordingProductCategory(e), + Warehouse: recordingWarehouseDTO(e), + Depletions: ToRecordingDepletionDTOs(e.Depletions), + Stocks: ToRecordingStockDTOs(e.Stocks), + Eggs: ToRecordingEggDTOs(e.Eggs), } } @@ -202,7 +212,8 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO { CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, - Warehouse: recordingWarehouseDTO(e), + Kandang: recordingKandangDTO(e), + Location: recordingKandangLocationDTO(e), } } @@ -214,20 +225,20 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { } return RecordingRelationDTO{ - Id: e.Id, - ProjectFlock: toRecordingProjectFlockDTO(e), - RecordDatetime: e.RecordDatetime, - Day: intValue(e.Day), - TotalDepletionQty: floatValue(e.TotalDepletionQty), - CumDepletionRate: floatValue(e.CumDepletionRate), - CumIntake: intValue(e.CumIntake), - FcrValue: floatValue(e.FcrValue), - HenDay: floatValue(e.HenDay), - HenHouse: floatValue(e.HenHouse), - FeedIntake: floatValue(e.FeedIntake), - EggMass: floatValue(e.EggMass), - EggWeight: floatValue(e.EggWeight), - Approval: latestApproval, + Id: e.Id, + ProjectFlock: toRecordingProjectFlockDTO(e), + RecordDatetime: e.RecordDatetime, + Day: intValue(e.Day), + TotalDepletionQty: floatValue(e.TotalDepletionQty), + CumDepletionRate: floatValue(e.CumDepletionRate), + CumIntake: intValue(e.CumIntake), + FcrValue: floatValue(e.FcrValue), + HenDay: floatValue(e.HenDay), + HenHouse: floatValue(e.HenHouse), + FeedIntake: floatValue(e.FeedIntake), + EggMass: floatValue(e.EggMass), + EggWeight: floatValue(e.EggWeight), + Approval: latestApproval, } } @@ -321,6 +332,34 @@ func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO { return mapWarehouseDTO(&pw.Warehouse) } +func recordingKandangDTO(e entity.Recording) *RecordingKandangDTO { + if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 { + return nil + } + kandang := e.ProjectFlockKandang.Kandang + return &RecordingKandangDTO{ + Id: kandang.Id, + Name: kandang.Name, + Status: kandang.Status, + Capacity: kandang.Capacity, + } +} + +func recordingKandangLocationDTO(e entity.Recording) *RecordingLocationDTO { + if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 { + return nil + } + location := e.ProjectFlockKandang.Kandang.Location + if location.Id == 0 { + return nil + } + return &RecordingLocationDTO{ + Id: location.Id, + Name: location.Name, + Address: location.Address, + } +} + func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse { if len(e.Stocks) > 0 { pw := e.Stocks[0].ProductWarehouse diff --git a/internal/modules/production/recordings/module.go b/internal/modules/production/recordings/module.go index 11a1e152..71981a9e 100644 --- a/internal/modules/production/recordings/module.go +++ b/internal/modules/production/recordings/module.go @@ -74,6 +74,28 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate panic(fmt.Sprintf("failed to register recording usable workflow: %v", err)) } } + if err := fifoService.RegisterUsable(fifo.UsableConfig{ + Key: fifo.UsableKeyRecordingDepletion, + Table: "recording_depletions", + Columns: fifo.UsableColumns{ + ID: "id", + ProductWarehouseID: "source_product_warehouse_id", + UsageQuantity: "qty", + PendingQuantity: "pending_qty", + CreatedAt: "id", + }, + ExcludedStockables: []fifo.StockableKey{ + fifo.StockableKeyTransferToLayingIn, + fifo.StockableKeyStockTransferIn, + fifo.StockableKeyAdjustmentIn, + fifo.StockableKeyPurchaseItems, + fifo.StockableKeyRecordingEgg, + }, + }); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "already registered") { + panic(fmt.Sprintf("failed to register recording depletion usable workflow: %v", err)) + } + } approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 703c05f0..9e783134 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -17,6 +17,7 @@ type RecordingRepository interface { repository.BaseRepository[entity.Recording] WithRelations(db *gorm.DB) *gorm.DB + ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) @@ -24,6 +25,7 @@ type RecordingRepository interface { DeleteStocks(tx *gorm.DB, recordingID uint) error ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error + UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error DeleteDepletions(tx *gorm.DB, recordingID uint) error @@ -84,6 +86,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB { Preload("CreatedUser"). Preload("ProjectFlockKandang"). Preload("ProjectFlockKandang.Kandang"). + Preload("ProjectFlockKandang.Kandang.Location"). Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard"). Preload("ProjectFlockKandang.ProjectFlock.Fcr"). @@ -107,6 +110,42 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB { Preload("Eggs.ProductWarehouse.Warehouse.Location") } +func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB { + normalized := strings.ToLower(strings.TrimSpace(rawSearch)) + if normalized == "" { + return db + } + + likeQuery := "%" + normalized + "%" + subQuery := db.Session(&gorm.Session{NewDB: true}). + Table("recordings"). + Select("recordings.id"). + Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id"). + Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Joins("LEFT JOIN kandangs k ON k.id = pfk.kandang_id"). + Joins("LEFT JOIN locations l ON l.id = k.location_id"). + Joins("LEFT JOIN recording_stocks rs ON rs.recording_id = recordings.id"). + Joins("LEFT JOIN recording_depletions rd ON rd.recording_id = recordings.id"). + Joins("LEFT JOIN recording_eggs re ON re.recording_id = recordings.id"). + Joins("LEFT JOIN product_warehouses pws ON pws.id = rs.product_warehouse_id"). + Joins("LEFT JOIN product_warehouses pwd ON pwd.id = rd.product_warehouse_id"). + Joins("LEFT JOIN product_warehouses pwe ON pwe.id = re.product_warehouse_id"). + Joins("LEFT JOIN warehouses ws ON ws.id = pws.warehouse_id"). + Joins("LEFT JOIN warehouses wd ON wd.id = pwd.warehouse_id"). + Joins("LEFT JOIN warehouses we ON we.id = pwe.warehouse_id"). + Where(` + LOWER(pf.flock_name) LIKE ? + OR LOWER(k.name) LIKE ? + OR LOWER(l.name) LIKE ? + OR LOWER(l.address) LIKE ? + OR LOWER(ws.name) LIKE ? + OR LOWER(wd.name) LIKE ? + OR LOWER(we.name) LIKE ?`, + likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, + ) + return db.Where("recordings.id IN (?)", subQuery) +} + func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) { if projectFlockKandangId == 0 { return nil, errors.New("project_flock_kandang_id is required") @@ -167,6 +206,12 @@ func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, us }).Error } +func (r *RecordingRepositoryImpl) UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error { + return tx.Model(&entity.RecordingDepletion{}). + Where("id = ?", depletionID). + Update("pending_qty", pendingQty).Error +} + func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error { if len(depletions) == 0 { return nil @@ -322,38 +367,25 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm. } func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) { - var rows []struct { + var result struct { TotalQty float64 - UomName string } if err := tx. Table("recording_stocks"). - Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name"). + Select("COALESCE(SUM(COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0)), 0) AS total_qty"). Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id"). Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN uoms ON uoms.id = products.uom_id"). Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN"). Where("recording_stocks.recording_id = ?", recordingID). - Scan(&rows).Error; err != nil { + Scan(&result).Error; err != nil { return 0, err } - var total float64 - for _, row := range rows { - if row.TotalQty <= 0 { - continue - } - switch strings.TrimSpace(row.UomName) { - case "kilogram", "kg", "kilograms", "kilo": - total += row.TotalQty * 1000 - case "gram", "g", "grams": - total += row.TotalQty - default: - total += row.TotalQty - } + if result.TotalQty <= 0 { + return 0, nil } - return total, nil + return result.TotalQty * 1000, nil } func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) { diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 5dabad9f..80611109 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -44,6 +44,7 @@ type RecordingFIFOIntegrationService interface { } var recordingStockUsableKey = fifo.UsableKeyRecordingStock +var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion type recordingService struct { Log *logrus.Logger @@ -116,7 +117,8 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti if params.ProjectFlockKandangId != 0 { db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId) } - return db.Order("record_datetime DESC").Order("created_at DESC") + db = s.Repository.ApplySearchFilters(db, params.Search) + return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC") }) if err != nil { @@ -209,9 +211,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if !isLaying && len(req.Eggs) > 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") } - if isLaying && len(req.Eggs) == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks") - } if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil { return nil, err @@ -280,10 +279,24 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions) + if s.FifoSvc != nil && len(mappedDepletions) > 0 { + sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId) + if err != nil { + return err + } + for i := range mappedDepletions { + mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID + } + } if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { s.Log.Errorf("Failed to persist depletions: %+v", err) return err } + if s.FifoSvc != nil { + if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil { + return err + } + } mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs) if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { @@ -297,11 +310,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } var warehouseDeltas map[uint]float64 - if s.FifoSvc != nil { - warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, nil) - } else { - warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs) - } + warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs) if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil { s.Log.Errorf("Failed to adjust product warehouses: %+v", err) return err @@ -407,9 +416,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if !isLaying && len(req.Eggs) > 0 { return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") } - if isLaying && len(req.Eggs) == 0 { - return fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks") - } } if hasStockChanges { @@ -431,17 +437,38 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } if hasDepletionChanges { + if s.FifoSvc != nil { + if err := s.releaseRecordingDepletions(ctx, tx, existingDepletions); err != nil { + return err + } + } + if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil { s.Log.Errorf("Failed to clear depletions: %+v", err) return err } mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions) + if s.FifoSvc != nil && len(mappedDepletions) > 0 { + sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId) + if err != nil { + return err + } + for i := range mappedDepletions { + mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID + } + } if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { s.Log.Errorf("Failed to update depletions: %+v", err) return err } + if s.FifoSvc != nil { + if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil { + return err + } + } + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil { s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err) return err @@ -647,6 +674,11 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { s.Log.Errorf("Failed to list depletions before delete: %+v", err) return err } + if s.FifoSvc != nil { + if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions); err != nil { + return err + } + } oldEggs, err := s.Repository.ListEggs(tx, id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -765,6 +797,46 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm. return nil } +func (s *recordingService) consumeRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error { + if len(depletions) == 0 || s.FifoSvc == nil { + return nil + } + + for _, depletion := range depletions { + if depletion.Id == 0 { + continue + } + + sourceWarehouseID := uint(0) + if depletion.SourceProductWarehouseId != nil { + sourceWarehouseID = *depletion.SourceProductWarehouseId + } + if sourceWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") + } + + desired := depletion.Qty + depletion.PendingQty + result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ + UsableKey: recordingDepletionUsableKey, + UsableID: depletion.Id, + ProductWarehouseID: sourceWarehouseID, + Quantity: desired, + AllowPending: false, + Tx: tx, + }) + if err != nil { + s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err) + return err + } + + if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil { + return err + } + } + + return nil +} + func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { return s.consumeRecordingStocks(ctx, tx, stocks) } @@ -796,10 +868,67 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm. return nil } +func (s *recordingService) releaseRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error { + if len(depletions) == 0 || s.FifoSvc == nil { + return nil + } + + for _, depletion := range depletions { + if depletion.Id == 0 { + continue + } + + sourceWarehouseID := uint(0) + if depletion.SourceProductWarehouseId != nil { + sourceWarehouseID = *depletion.SourceProductWarehouseId + } + if sourceWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") + } + + if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ + UsableKey: recordingDepletionUsableKey, + UsableID: depletion.Id, + Tx: tx, + }); err != nil { + s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err) + return err + } + + if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil { + return err + } + } + + return nil +} + func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { return s.releaseRecordingStocks(ctx, tx, stocks) } +func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) { + if projectFlockKandangID == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid") + } + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) + if err != nil { + s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi") + } + for _, pop := range populations { + if pop.ProductWarehouseId > 0 && pop.TotalQty > 0 { + return pop.ProductWarehouseId, nil + } + } + for _, pop := range populations { + if pop.ProductWarehouseId > 0 { + return pop.ProductWarehouseId, nil + } + } + return 0, fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan") +} + func buildWarehouseDeltas( oldDepletions, newDepletions []entity.RecordingDepletion, oldEggs, newEggs []entity.RecordingEgg, @@ -941,10 +1070,8 @@ func (s *recordingService) syncRecordingStocks( desired := item.Qty stock.UsageQty = &desired - if item.PendingQty != nil { - pending := *item.PendingQty - stock.PendingQty = &pending - } + zero := 0.0 + stock.PendingQty = &zero stocksToConsume = append(stocksToConsume, stock) } @@ -990,43 +1117,20 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error { } func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { - hasPending := false - for _, item := range incoming { - if item.PendingQty != nil { - hasPending = true - break - } - } - existingUsage := make(map[uint]float64) - existingTotal := make(map[uint]float64) for _, stock := range existing { var usage float64 - var pending float64 if stock.UsageQty != nil { usage = *stock.UsageQty } - if stock.PendingQty != nil { - pending = *stock.PendingQty - } existingUsage[stock.ProductWarehouseId] += usage - existingTotal[stock.ProductWarehouseId] += usage + pending } incomingUsage := make(map[uint]float64) - incomingTotal := make(map[uint]float64) for _, item := range incoming { - var pending float64 - if item.PendingQty != nil { - pending = *item.PendingQty - } incomingUsage[item.ProductWarehouseId] += item.Qty - incomingTotal[item.ProductWarehouseId] += item.Qty + pending } - if hasPending { - return floatMapsMatch(existingTotal, incomingTotal) - } return floatMapsMatch(existingUsage, incomingUsage) } @@ -1224,7 +1328,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm var eggMass float64 if remainingChick > 0 && totalEggWeightGrams > 0 { - eggMass = (totalEggWeightGrams / remainingChick) * 1000 + eggMass = totalEggWeightGrams / remainingChick updates["egg_mass"] = eggMass recording.EggMass = &eggMass } else { @@ -1234,7 +1338,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm var eggWeight float64 if totalEggQty > 0 && totalEggWeightGrams > 0 { - eggWeight = (totalEggWeightGrams / totalEggQty) * 1000 + eggWeight = totalEggWeightGrams / totalEggQty updates["egg_weight"] = eggWeight recording.EggWeight = &eggWeight } else { diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index 8b4eab57..dbbd4f30 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -2,9 +2,8 @@ package validation type ( Stock struct { - ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` - Qty float64 `json:"qty" validate:"required,gte=0"` - PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"` + ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` + Qty float64 `json:"qty" validate:"required,gte=0"` } Depletion struct { @@ -20,23 +19,24 @@ type ( ) type Create struct { - ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` - RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"` - Stocks []Stock `json:"stocks" validate:"dive"` - Depletions []Depletion `json:"depletions" validate:"dive"` - Eggs []Egg `json:"eggs" validate:"omitempty,dive"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` + RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"` + Stocks []Stock `json:"stocks" validate:"dive"` + Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` + Eggs []Egg `json:"eggs" validate:"omitempty,dive"` } type Update struct { - Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` - Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` - Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"` + Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` + Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` + Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"` } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` - ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` + Search string `query:"search" validate:"omitempty,max=50"` } type Approve struct { diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 92db84a3..41611ac3 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -345,7 +345,52 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file ); err != nil { return nil, err } - if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil { + + pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") + } + return nil, err + } + category := strings.TrimSpace(pfk.ProjectFlock.Category) + if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 { + if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil { + if strings.TrimSpace(standard.ProjectCategory) != "" { + category = standard.ProjectCategory + } + } + } + weekBase := 1 + if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) { + weekBase = 18 + } + if req.Week < weekBase { + if weekBase == 18 { + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects") + } + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") + } + + var latestWeek int + if err := s.Repository.DB().WithContext(c.Context()). + Model(&entity.ProjectFlockKandangUniformity{}). + Where("project_flock_kandang_id = ? AND deleted_at IS NULL", req.ProjectFlockKandangId). + Select("COALESCE(MAX(week), 0)"). + Scan(&latestWeek).Error; err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence") + } + if latestWeek == 0 && req.Week != weekBase { + if weekBase == 18 { + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects") + } + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") + } + if latestWeek > 0 && req.Week > latestWeek+1 { + return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping") + } + + if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week); err != nil { return nil, err } @@ -487,8 +532,35 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui if req.ProjectFlockKandangId != nil { targetPFKID = *req.ProjectFlockKandangId } + if targetPFKID != 0 && targetWeek > 0 { + pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetPFKID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") + } + return nil, err + } + category := strings.TrimSpace(pfk.ProjectFlock.Category) + if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 { + if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil { + if strings.TrimSpace(standard.ProjectCategory) != "" { + category = standard.ProjectCategory + } + } + } + weekBase := 1 + if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) { + weekBase = 18 + } + if targetWeek < weekBase { + if weekBase == 18 { + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects") + } + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") + } + } if targetDate != nil { - if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil { + if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek); err != nil { return nil, err } } @@ -604,7 +676,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui return s.GetOne(c, id) } -func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error { +func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int) error { if projectFlockKandangID == 0 || week == 0 { return nil } diff --git a/internal/modules/repports/dto/repportPurchase.dto.go b/internal/modules/repports/dto/repportPurchase.dto.go index 830a076f..f776121b 100644 --- a/internal/modules/repports/dto/repportPurchase.dto.go +++ b/internal/modules/repports/dto/repportPurchase.dto.go @@ -27,12 +27,12 @@ type PurchaseSupplierRowDTO struct { } type PurchaseSupplierSummaryDTO struct { - TotalQty float64 `json:"total_qty"` - TotalPurchaseValue float64 `json:"total_purchase_value"` - TotalTransportValue float64 `json:"total_transport_value"` - TotalAmount float64 `json:"total_amount"` - TotalUnitPrice float64 `json:"total_unit_price"` - TotalTransportUnitPrice float64 `json:"total_transport_unit_price"` + TotalQty float64 `json:"total_qty"` + TotalPurchaseValue float64 `json:"total_purchase_value"` + TotalTransportValue float64 `json:"total_transport_value"` + TotalAmount float64 `json:"total_amount"` + TotalUnitPrice float64 `json:"total_unit_price"` + TotalTransportUnitPrice float64 `json:"total_transport_unit_price"` } type PurchaseSupplierDTO struct { @@ -122,11 +122,6 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem rows := make([]PurchaseSupplierRowDTO, 0, len(items)) summary := PurchaseSupplierSummaryDTO{} - var unitPriceSum float64 - var unitPriceCount int - var transportUnitPriceSum float64 - var transportUnitPriceCount int - for i := range items { row := ToPurchaseSupplierRowDTO(&items[i]) rows = append(rows, row) @@ -136,19 +131,16 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem summary.TotalTransportValue += row.TransportValue summary.TotalAmount += row.TotalAmount - unitPriceSum += row.UnitPrice - unitPriceCount++ - - transportUnitPriceSum += row.TransportUnitPrice - transportUnitPriceCount++ } - if unitPriceCount > 0 { - summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount)) + if summary.TotalQty > 0 { + avg := summary.TotalPurchaseValue / summary.TotalQty + summary.TotalUnitPrice = math.Round(avg) } - if transportUnitPriceCount > 0 { - summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount)) + if summary.TotalQty > 0 { + avg := summary.TotalTransportValue / summary.TotalQty + summary.TotalTransportUnitPrice = math.Round(avg) } return PurchaseSupplierDTO{ diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go index e8f548d6..977db610 100644 --- a/internal/modules/repports/repositories/debt_supplier.repository.go +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -17,6 +18,8 @@ type DebtSupplierRepository interface { GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error) + GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error) + GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) } @@ -25,6 +28,11 @@ type debtSupplierRepositoryImpl struct { db *gorm.DB } +type PaymentReferenceSummary struct { + Total float64 + LatestPaymentDate time.Time +} + func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository { return &debtSupplierRepositoryImpl{db: db} } @@ -167,7 +175,8 @@ func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context, Model(&entity.Payment{}). Where("party_type = ?", string(utils.PaymentPartySupplier)). Where("direction = ?", "OUT"). - Where("party_id IN ?", supplierIDs) + Where("party_id IN ?", supplierIDs). + Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)) if strings.TrimSpace(filters.StartDate) != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { @@ -238,6 +247,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co Where("direction = ?", "OUT"). Where("party_id IN ?", supplierIDs). Where("reference_number IN ?", references). + Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)). Group("reference_number"). Scan(&rows).Error; err != nil { return nil, err @@ -254,6 +264,75 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co return result, nil } +func (r *debtSupplierRepositoryImpl) GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error) { + if len(supplierIDs) == 0 || len(references) == 0 { + return map[string]PaymentReferenceSummary{}, nil + } + + type paymentRow struct { + ReferenceNumber *string `gorm:"column:reference_number"` + Total float64 `gorm:"column:total"` + LatestPaymentDate time.Time `gorm:"column:latest_payment_date"` + } + + rows := make([]paymentRow, 0) + if err := r.db.WithContext(ctx). + Model(&entity.Payment{}). + Select("reference_number, SUM(nominal) AS total, MAX(payment_date) AS latest_payment_date"). + Where("party_type = ?", string(utils.PaymentPartySupplier)). + Where("direction = ?", "OUT"). + Where("party_id IN ?", supplierIDs). + Where("reference_number IN ?", references). + Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)). + Group("reference_number"). + Scan(&rows).Error; err != nil { + return nil, err + } + + result := make(map[string]PaymentReferenceSummary, len(rows)) + for _, row := range rows { + if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" { + continue + } + result[*row.ReferenceNumber] = PaymentReferenceSummary{ + Total: row.Total, + LatestPaymentDate: row.LatestPaymentDate, + } + } + + return result, nil +} + +func (r *debtSupplierRepositoryImpl) GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error) { + if len(supplierIDs) == 0 { + return map[uint]float64{}, nil + } + + type balanceRow struct { + SupplierID uint `gorm:"column:supplier_id"` + Total float64 `gorm:"column:total"` + } + + rows := make([]balanceRow, 0) + if err := r.db.WithContext(ctx). + Model(&entity.Payment{}). + Select("party_id AS supplier_id, SUM(nominal) AS total"). + Where("party_type = ?", string(utils.PaymentPartySupplier)). + Where("party_id IN ?", supplierIDs). + Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)). + Group("party_id"). + Scan(&rows).Error; err != nil { + return nil, err + } + + result := make(map[uint]float64, len(rows)) + for _, row := range rows { + result[row.SupplierID] = row.Total + } + + return result, nil +} + func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) { if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" { return map[uint]float64{}, nil @@ -313,6 +392,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont Where("party_type = ?", string(utils.PaymentPartySupplier)). Where("direction = ?", "OUT"). Where("party_id IN ?", supplierIDs). + Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)). Where("DATE(payment_date) < ?", dateFrom). Group("party_id"). Scan(&rows).Error; err != nil { diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go index 979623fc..6a07c555 100644 --- a/internal/modules/repports/repositories/purchase_supplier.repository.go +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -25,6 +25,21 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository { return &purchaseSupplierRepositoryImpl{db: db} } +func (r *purchaseSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB { + return r.db.WithContext(ctx). + Table("approvals AS a"). + Select("a.approvable_id, a.step_number, a.action"). + Joins(` + JOIN ( + SELECT approvable_id, MAX(action_at) AS latest_action_at + FROM approvals + WHERE approvable_type = ? + GROUP BY approvable_id + ) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`, + string(utils.ApprovalWorkflowPurchase), + ) +} + func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { dateColumn := "purchase_items.received_date" switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { @@ -34,10 +49,16 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, dateColumn = "purchase_items.received_date" } + latestApproval := r.latestPurchaseApproval(ctx) + db := r.db.WithContext(ctx). Model(&entity.Supplier{}). Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). - Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id") + Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", latestApproval). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL") if filters.SupplierId > 0 { db = db.Where("suppliers.id = ?", filters.SupplierId) @@ -152,7 +173,11 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context Preload("ExpenseNonstock.Expense"). Preload("ExpenseNonstock.Expense.Supplier"). Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id"). - Where("purchases.supplier_id IN ?", supplierIDs) + Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). + Where("purchases.supplier_id IN ?", supplierIDs). + Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Where("purchase_items.received_date IS NOT NULL") if filters.ProductId > 0 { db = db.Where("purchase_items.product_id = ?", filters.ProductId) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 41a66731..a0e0f350 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1129,6 +1129,17 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu return nil, 0, err } + initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs) + if err != nil { + return nil, 0, err + } + + references := collectDebtSupplierReferences(purchases) + paymentSummaries, err := s.DebtSupplierRepo.GetPaymentSummariesByReferences(c.Context(), supplierIDs, references) + if err != nil { + return nil, 0, err + } + location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") @@ -1150,7 +1161,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu continue } - initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] + initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]) items := purchasesBySupplier[supplierID] paymentItems := paymentsBySupplier[supplierID] total := dto.DebtSupplierTotalDTO{} @@ -1158,6 +1169,16 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems)) for _, purchase := range items { row := buildDebtSupplierRow(purchase, now, location) + if reference := resolveDebtSupplierReference(purchase); reference != "" { + if summary, ok := paymentSummaries[reference]; ok { + if isDebtSupplierPaid(row.TotalPrice, summary.Total) { + row.Status = "Lunas" + if !summary.LatestPaymentDate.IsZero() { + row.Aging = calculateDebtSupplierAging(purchase, summary.LatestPaymentDate, location) + } + } + } + } sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) combinedRows = append(combinedRows, debtSupplierRowItem{ Row: row, @@ -1374,6 +1395,55 @@ func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc return purchase.CreatedAt.In(loc) } +func collectDebtSupplierReferences(purchases []entity.Purchase) []string { + if len(purchases) == 0 { + return nil + } + seen := make(map[string]struct{}, len(purchases)) + result := make([]string, 0, len(purchases)) + for _, purchase := range purchases { + ref := resolveDebtSupplierReference(purchase) + if ref == "" { + continue + } + if _, ok := seen[ref]; ok { + continue + } + seen[ref] = struct{}{} + result = append(result, ref) + } + return result +} + +func resolveDebtSupplierReference(purchase entity.Purchase) string { + if purchase.PoNumber != nil { + if ref := strings.TrimSpace(*purchase.PoNumber); ref != "" { + return ref + } + } + if ref := strings.TrimSpace(purchase.PrNumber); ref != "" { + return ref + } + return "" +} + +func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool { + if totalPrice <= 0 { + return true + } + return paymentTotal >= totalPrice-0.000001 +} + +func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int { + prDate := purchase.CreatedAt.In(loc) + startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc) + stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc) + if stopDate.Before(startDate) { + return 0 + } + return int(stopDate.Sub(startDate).Hours() / 24) +} + func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) { params, filters, err := s.parseHppPerKandangQuery(ctx) if err != nil { diff --git a/internal/utils/fifo/constants.go b/internal/utils/fifo/constants.go index 076d960d..840ba8e1 100644 --- a/internal/utils/fifo/constants.go +++ b/internal/utils/fifo/constants.go @@ -2,18 +2,19 @@ package fifo const ( // Usable Keys - UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" - UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" - UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" - UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT" - UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT" - UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT" + UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" + UsableKeyRecordingDepletion UsableKey = "RECORDING_DEPLETION" + UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" + UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" + UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT" + UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT" + UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT" // Stockable Keys - StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN" - StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN" - StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" - StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" + StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN" + StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN" + StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" + StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION" - StockableKeyRecordingEgg StockableKey = "RECORDING_EGG" + StockableKeyRecordingEgg StockableKey = "RECORDING_EGG" ) diff --git a/internal/utils/recording/util.recording.go b/internal/utils/recording/util.recording.go index 91c9cc4b..f40818bf 100644 --- a/internal/utils/recording/util.recording.go +++ b/internal/utils/recording/util.recording.go @@ -14,15 +14,10 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto for _, item := range items { usagePtr := new(float64) *usagePtr = item.Qty - pending := item.PendingQty - if pending == nil { - pending = new(float64) - } result = append(result, entity.RecordingStock{ RecordingId: recordingID, ProductWarehouseId: item.ProductWarehouseId, UsageQty: usagePtr, - PendingQty: pending, }) } return result