From b8425c0f589de727483789977a83c512fee32653 Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 11 Dec 2025 04:06:51 +0000 Subject: [PATCH 1/9] Edit .air.toml --- .air.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.air.toml b/.air.toml index 0c534172..c463b5b2 100644 --- a/.air.toml +++ b/.air.toml @@ -3,7 +3,7 @@ root = "." tmp_dir = "tmp" [build] -cmd = "go build -o ./tmp/main ./cmd/api" +cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api" bin = "tmp/main" full_bin = "APP_ENV=dev ./tmp/main" include_ext = ["go", "tpl", "tmpl", "html"] From cd739f41b94def0dbc651816711ea056e5d00590 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 16 Dec 2025 14:42:31 +0700 Subject: [PATCH 2/9] Feat(BE-339): make a report for purchasing supplier --- .../controllers/repport.controller.go | 50 +++++ .../repports/dto/repportPurchase.dto.go | 138 ++++++++++++ internal/modules/repports/module.go | 4 +- .../purchase_supplier.repository.go | 196 ++++++++++++++++++ internal/modules/repports/route.go | 1 + .../repports/services/repport.service.go | 69 +++++- .../validations/repport.validation.go | 13 ++ internal/response/response.go | 9 +- 8 files changed, 474 insertions(+), 6 deletions(-) create mode 100644 internal/modules/repports/dto/repportPurchase.dto.go create mode 100644 internal/modules/repports/repositories/purchase_supplier.repository.go diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 21d3c49a..3e6c39d0 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -97,3 +97,53 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { Data: result, }) } + +func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { + query := &validation.PurchaseSupplierQuery{ + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + AreaId: int64(ctx.QueryInt("area_id", 0)), + SupplierId: int64(ctx.QueryInt("supplier_id", 0)), + ProductId: int64(ctx.QueryInt("product_id", 0)), + ProductCategoryId: int64(ctx.QueryInt("product_category_id", 0)), + DateFrom: ctx.Query("date_from", ""), + DateTo: ctx.Query("date_to", ""), + SortBy: ctx.Query("sort_by", ""), + FilterBy: ctx.Query("filter_by", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := c.RepportService.GetPurchaseSupplier(ctx, query) + if err != nil { + return err + } + + filters := map[string]interface{}{ + "area_id": query.AreaId, + "supplier_id": query.SupplierId, + "product_id": query.ProductId, + "product_category_id": query.ProductCategoryId, + "date_from": query.DateFrom, + "date_to": query.DateTo, + "sort_by": query.SortBy, + "filter_by": query.FilterBy, + } + + return ctx.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.PurchaseSupplierDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get supplier purchase recap successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + Filters: filters, + }, + Data: result, + }) +} diff --git a/internal/modules/repports/dto/repportPurchase.dto.go b/internal/modules/repports/dto/repportPurchase.dto.go new file mode 100644 index 00000000..60fd0fee --- /dev/null +++ b/internal/modules/repports/dto/repportPurchase.dto.go @@ -0,0 +1,138 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" +) + +type PurchaseSupplierRowDTO struct { + ReceiveDate string `json:"receive_date"` + PoDate string `json:"po_date"` + PoNumber string `json:"po_number"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + PurchaseValue float64 `json:"purchase_value"` + TransportUnitPrice float64 `json:"transport_unit_price"` + TransportValue float64 `json:"transport_value"` + TotalAmount float64 `json:"total_amount"` + Expedition string `json:"expedition"` + DeliveryNumber string `json:"delivery_number"` +} + +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"` +} + +type PurchaseSupplierDTO struct { + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + Rows []PurchaseSupplierRowDTO `json:"rows"` + Summary PurchaseSupplierSummaryDTO `json:"summary"` +} + +func formatDatePtr(t *time.Time) string { + if t == nil || t.IsZero() { + return "" + } + return t.Format("02-Jan-2006") +} + +func ToPurchaseSupplierRowDTO(item *entity.PurchaseItem) PurchaseSupplierRowDTO { + row := PurchaseSupplierRowDTO{ + ReceiveDate: formatDatePtr(item.ReceivedDate), + Qty: item.TotalQty, + UnitPrice: item.Price, + } + + if item.Purchase != nil { + row.PoDate = formatDatePtr(item.Purchase.PoDate) + if item.Purchase.PoNumber != nil { + row.PoNumber = *item.Purchase.PoNumber + } + } + + if item.Product != nil && item.Product.Id != 0 { + product := productDTO.ToProductRelationDTO(*item.Product) + row.Product = &product + } + + if item.Warehouse != nil && item.Warehouse.Id != 0 { + warehouse := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse) + row.Warehouse = &warehouse + } + + qty := row.Qty + if qty < 0 { + qty = 0 + } + + row.PurchaseValue = row.UnitPrice * qty + + var transportUnit float64 + var expeditionName string + + if item.ExpenseNonstock != nil { + transportUnit = item.ExpenseNonstock.Price + + if item.ExpenseNonstock.Expense != nil && + item.ExpenseNonstock.Expense.Supplier != nil && + item.ExpenseNonstock.Expense.Supplier.Id != 0 { + expSupplier := item.ExpenseNonstock.Expense.Supplier + expeditionName = expSupplier.Name + } + } + + row.TransportUnitPrice = transportUnit + row.TransportValue = transportUnit * qty + row.TotalAmount = row.PurchaseValue + row.TransportValue + + if expeditionName == "" { + row.Expedition = "-" + } else { + row.Expedition = expeditionName + } + + if item.TravelNumber != nil && *item.TravelNumber != "" { + row.DeliveryNumber = *item.TravelNumber + } else { + row.DeliveryNumber = "-" + } + + return row +} + +func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem) PurchaseSupplierDTO { + var supplierDTORef *supplierDTO.SupplierRelationDTO + if supplier.Id != 0 { + mapped := supplierDTO.ToSupplierRelationDTO(supplier) + supplierDTORef = &mapped + } + + rows := make([]PurchaseSupplierRowDTO, 0, len(items)) + summary := PurchaseSupplierSummaryDTO{} + + for i := range items { + row := ToPurchaseSupplierRowDTO(&items[i]) + rows = append(rows, row) + + summary.TotalQty += row.Qty + summary.TotalPurchaseValue += row.PurchaseValue + summary.TotalTransportValue += row.TransportValue + summary.TotalAmount += row.TotalAmount + } + + return PurchaseSupplierDTO{ + Supplier: supplierDTORef, + Rows: rows, + Summary: summary, + } +} + diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 4479b733..f3798f6a 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -7,6 +7,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" @@ -20,9 +21,10 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) + purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc, purchaseSupplierRepository) RepportRoutes(router, repportService) } diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go new file mode 100644 index 00000000..cd282e8e --- /dev/null +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -0,0 +1,196 @@ +package repositories + +import ( + "context" + "fmt" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "gorm.io/gorm" +) + +type PurchaseSupplierRepository interface { + GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error) + GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error) +} + +type purchaseSupplierRepositoryImpl struct { + db *gorm.DB +} + +func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository { + return &purchaseSupplierRepositoryImpl{db: db} +} + +func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { + // Tentukan kolom tanggal yang akan dipakai untuk filter + dateColumn := "purchase_items.received_date" + switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { + case "po_date": + dateColumn = "purchases.po_date" + case "receive_date", "": + dateColumn = "purchase_items.received_date" + } + + 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") + + if filters.SupplierId > 0 { + db = db.Where("suppliers.id = ?", filters.SupplierId) + } + + if filters.ProductId > 0 { + db = db.Where("purchase_items.product_id = ?", filters.ProductId) + } + + if filters.ProductCategoryId > 0 { + db = db. + Joins("JOIN products ON products.id = purchase_items.product_id"). + Where("products.product_category_id = ?", filters.ProductCategoryId) + } + + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) + } + + if filters.DateFrom != "" { + if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.DateTo != "" { + if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + return db +} + +func (r *purchaseSupplierRepositoryImpl) GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error) { + query := r.baseSupplierQuery(ctx, filters) + + var totalSuppliers int64 + if err := query. + Distinct("suppliers.id"). + Count(&totalSuppliers).Error; err != nil { + return nil, 0, err + } + + if totalSuppliers == 0 { + return []entity.Supplier{}, 0, nil + } + + if offset < 0 { + offset = 0 + } + + var supplierIDs []uint + if err := query. + Select("suppliers.id"). + Order("suppliers.id ASC"). + Offset(offset). + Limit(limit). + Pluck("suppliers.id", &supplierIDs).Error; err != nil { + return nil, 0, err + } + + if len(supplierIDs) == 0 { + return []entity.Supplier{}, totalSuppliers, nil + } + + var suppliers []entity.Supplier + if err := r.db.WithContext(ctx). + Where("id IN ?", supplierIDs). + Find(&suppliers).Error; err != nil { + return nil, 0, err + } + + return suppliers, totalSuppliers, nil +} + +func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error) { + if len(supplierIDs) == 0 { + return []entity.PurchaseItem{}, nil + } + + // Tentukan kolom tanggal yang akan dipakai untuk filter & sort + dateColumn := "purchase_items.received_date" + switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { + case "po_date": + dateColumn = "purchases.po_date" + case "receive_date", "": + dateColumn = "purchase_items.received_date" + } + + orderDirection := "ASC" + switch strings.ToUpper(strings.TrimSpace(filters.SortBy)) { + case "DESC": + orderDirection = "DESC" + case "ASC", "": + orderDirection = "ASC" + } + + db := r.db.WithContext(ctx). + Model(&entity.PurchaseItem{}). + Preload("Purchase"). + Preload("Purchase.Supplier"). + Preload("Product"). + Preload("Product.ProductCategory"). + Preload("Warehouse"). + Preload("Warehouse.Area"). + Preload("Warehouse.Location"). + Preload("Warehouse.Kandang"). + Preload("ExpenseNonstock"). + Preload("ExpenseNonstock.Expense"). + Preload("ExpenseNonstock.Expense.Supplier"). + Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id"). + Where("purchases.supplier_id IN ?", supplierIDs) + + if filters.ProductId > 0 { + db = db.Where("purchase_items.product_id = ?", filters.ProductId) + } + + if filters.ProductCategoryId > 0 { + db = db. + Joins("JOIN products ON products.id = purchase_items.product_id"). + Where("products.product_category_id = ?", filters.ProductCategoryId) + } + + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) + } + + if filters.DateFrom != "" { + if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.DateTo != "" { + if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + // Urutkan berdasarkan kolom tanggal yang dipilih dan arah sort + db = db.Order(fmt.Sprintf("%s %s", dateColumn, orderDirection)). + Order("purchase_items.id ASC") + + var items []entity.PurchaseItem + if err := db.Find(&items).Error; err != nil { + return nil, err + } + + return items, nil +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 4aea831c..d24caac5 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -14,4 +14,5 @@ func RepportRoutes(v1 fiber.Router, s repport.RepportService) { route.Get("/expense", ctrl.GetExpense) route.Get("/marketing", ctrl.GetMarketing) + route.Get("/purchase-supplier", ctrl.GetPurchaseSupplier) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 3adc5c0a..aa649871 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -2,6 +2,7 @@ package service import ( "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -10,6 +11,8 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" @@ -19,6 +22,7 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) + GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) } type repportService struct { @@ -27,15 +31,23 @@ type repportService struct { ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository ApprovalSvc approvalService.ApprovalService + PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService { +func NewRepportService( + validate *validator.Validate, + expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, + marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, + approvalSvc approvalService.ApprovalService, + purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, +) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, ApprovalSvc: approvalSvc, + PurchaseSupplierRepo: purchaseSupplierRepo, } } @@ -113,3 +125,58 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil } + +func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + if offset < 0 { + offset = 0 + } + + suppliers, totalSuppliers, err := s.PurchaseSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params) + if err != nil { + return nil, 0, err + } + + if totalSuppliers == 0 || len(suppliers) == 0 { + return []dto.PurchaseSupplierDTO{}, totalSuppliers, nil + } + + supplierMap := make(map[uint]entity.Supplier, len(suppliers)) + supplierIDs := make([]uint, 0, len(suppliers)) + for _, supplier := range suppliers { + supplierMap[supplier.Id] = supplier + supplierIDs = append(supplierIDs, supplier.Id) + } + + items, err := s.PurchaseSupplierRepo.GetItemsBySuppliers(c.Context(), supplierIDs, params) + if err != nil { + return nil, 0, err + } + + itemsBySupplier := make(map[uint][]entity.PurchaseItem) + for _, item := range items { + if item.Purchase == nil { + continue + } + supplierID := item.Purchase.SupplierId + itemsBySupplier[supplierID] = append(itemsBySupplier[supplierID], item) + } + + result := make([]dto.PurchaseSupplierDTO, 0, len(supplierIDs)) + for _, supplierID := range supplierIDs { + supplier, exists := supplierMap[supplierID] + if !exists { + continue + } + + supplierItems := itemsBySupplier[supplierID] + dtoItem := dto.ToPurchaseSupplierDTO(supplier, supplierItems) + result = append(result, dtoItem) + } + + return result, totalSuppliers, nil +} diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 7efc51f9..942eeaa8 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -27,3 +27,16 @@ type MarketingQuery struct { SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` MarketingId int64 `query:"marketing_id" validate:"omitempty"` } + +type PurchaseSupplierQuery struct { + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + AreaId int64 `query:"area_id" validate:"omitempty"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` + DateFrom string `query:"date_from" validate:"omitempty"` + DateTo string `query:"date_to" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty"` +} diff --git a/internal/response/response.go b/internal/response/response.go index c4ecca0f..710d320e 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -14,10 +14,11 @@ type Success struct { } type Meta struct { - Page int `json:"page"` - Limit int `json:"limit"` - TotalPages int64 `json:"total_pages"` - TotalResults int64 `json:"total_results"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int64 `json:"total_pages"` + TotalResults int64 `json:"total_results"` + Filters interface{} `json:"filters,omitempty"` } type SuccessWithPaginate[T any] struct { From 21d22c20a3addb5d654b4f9ec7fd5bdfc8730eef Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Wed, 17 Dec 2025 13:20:00 +0700 Subject: [PATCH 3/9] add constant flag --- internal/utils/constant.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 6594ac6b..8b51619b 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -29,6 +29,18 @@ const ( FlagVitamin FlagType = "VITAMIN" FlagKimia FlagType = "KIMIA" FlagEkspedisi FlagType = "EKSPEDISI" + + // flag ayam + FlagAyamAfkir FlagType = "AYAM-AFKIR" + FlagAyamCulling FlagType = "AYAM-CULLING" + FlagAyamMati FlagType = "AYAM-MATI" + + //flag telur + FlagTelur FlagType = "TELUR" + FlagTelurUtuh FlagType = "TELUR-UTUH" + FlagTelurPecah FlagType = "TELUR-PECAH" + FlagTelurPutih FlagType = "TELUR-PUTIH" + FlagTelurRetak FlagType = "TELUR-RETAK" ) const ( @@ -205,8 +217,8 @@ const ( ) var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{ - RecordingStepPengajuan: "Pengajuan", - RecordingStepDisetujui: "Disetujui", + RecordingStepPengajuan: "Pengajuan", + RecordingStepDisetujui: "Disetujui", } // ------------------------------------------------------------------- From e52a02b1c0e90855f5ec35d1c5c0d9de4f8a21fd Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 18 Dec 2025 11:30:55 +0700 Subject: [PATCH 4/9] Feat(BE-339): make reporting purchase per supplier with filterization --- .../repports/controllers/repport.controller.go | 8 ++++---- .../purchase_supplier.repository.go | 17 ++++++++--------- .../repports/validations/repport.validation.go | 4 ++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 3e6c39d0..039854c8 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -106,8 +106,8 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { SupplierId: int64(ctx.QueryInt("supplier_id", 0)), ProductId: int64(ctx.QueryInt("product_id", 0)), ProductCategoryId: int64(ctx.QueryInt("product_category_id", 0)), - DateFrom: ctx.Query("date_from", ""), - DateTo: ctx.Query("date_to", ""), + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), SortBy: ctx.Query("sort_by", ""), FilterBy: ctx.Query("filter_by", ""), } @@ -126,8 +126,8 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { "supplier_id": query.SupplierId, "product_id": query.ProductId, "product_category_id": query.ProductCategoryId, - "date_from": query.DateFrom, - "date_to": query.DateTo, + "start_date": query.StartDate, + "end_date": query.EndDate, "sort_by": query.SortBy, "filter_by": query.FilterBy, } diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go index cd282e8e..979623fc 100644 --- a/internal/modules/repports/repositories/purchase_supplier.repository.go +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -26,7 +26,6 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository { } func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { - // Tentukan kolom tanggal yang akan dipakai untuk filter dateColumn := "purchase_items.received_date" switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { case "po_date": @@ -60,14 +59,14 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, Where("warehouses.area_id = ?", filters.AreaId) } - if filters.DateFrom != "" { - if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) } } - if filters.DateTo != "" { - if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) } } @@ -171,14 +170,14 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context Where("warehouses.area_id = ?", filters.AreaId) } - if filters.DateFrom != "" { - if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) } } - if filters.DateTo != "" { - if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) } } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 942eeaa8..53ba22d7 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -35,8 +35,8 @@ type PurchaseSupplierQuery struct { SupplierId int64 `query:"supplier_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"` ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` - DateFrom string `query:"date_from" validate:"omitempty"` - DateTo string `query:"date_to" validate:"omitempty"` + StartDate string `query:"start_date" validate:"omitempty"` + EndDate string `query:"end_date" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty"` FilterBy string `query:"filter_by" validate:"omitempty"` } From d675b1e82651fba37d76ba3a33e0cd5bddacb6bc Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Thu, 18 Dec 2025 13:32:48 +0700 Subject: [PATCH 5/9] feat[BE-375]: get api closing data produksi --- .../controllers/closing.controller.go | 22 ++ internal/modules/closings/dto/closing.dto.go | 46 ++++ .../repositories/closing.repository.go | 164 +++++++++++++ internal/modules/closings/route.go | 1 + .../closings/services/closing.service.go | 218 ++++++++++++++++++ 5 files changed, 451 insertions(+) diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index dc39a666..cd105a48 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -188,3 +188,25 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error { Data: result, }) } + +func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error { + param := c.Params("projectFlockId") + + id, err := strconv.Atoi(param) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") + } + + result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Retrieved production data successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index 1f1cb492..b3075776 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -58,6 +58,52 @@ type ClosingSummaryDTO struct { StatusClosing string `json:"closing_status"` } +type ClosingPurchaseDTO struct { + InitialPopulation int `json:"initial_population"` + ClaimCulling int `json:"claim_culling"` + FinalPopulation int `json:"final_population"` + FeedIn float64 `json:"feed_in"` + FeedUsed float64 `json:"feed_used"` + FeedUsedPerHead float64 `json:"feed_used_per_head"` +} + +type ClosingSalesDTO struct { + SalesPopulation int `json:"sales_population"` + SalesWeight float64 `json:"sales_weight"` + AverageWeight float64 `json:"average_weight"` + AverageSellingPrice float64 `json:"average_selling_price"` +} + +type ClosingEggSalesDTO struct { + EggPieces int `json:"egg_pieces"` + EggMassKg float64 `json:"egg_mass_kg"` + AverageEggWeightKg float64 `json:"average_egg_weight_kg"` + AverageSellingPrice float64 `json:"average_selling_price"` +} + +type ClosingPerformanceDTO struct { + Depletion float64 `json:"depletion"` + Age float64 `json:"age"` + MortalityStd float64 `json:"mortality_std"` + MortalityAct float64 `json:"mortality_act"` + DeffMortality float64 `json:"deff_mortality"` + FcrStd float64 `json:"fcr_std"` + FcrAct float64 `json:"fcr_act"` + DeffFcr float64 `json:"deff_fcr"` + Adg float64 `json:"adg"` +} + +type ClosingSalesGroupDTO struct { + ChickenProduction ClosingSalesDTO `json:"chicken_production"` + EggProduction ClosingEggSalesDTO `json:"egg_production"` +} + +type ClosingProductionReportDTO struct { + Purchase ClosingPurchaseDTO `json:"purchase"` + Sales ClosingSalesGroupDTO `json:"sales"` + Performance ClosingPerformanceDTO `json:"performance"` +} + func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO { history := project.KandangHistory diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index fe555378..186f48a2 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -9,12 +9,19 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) type ClosingRepository interface { repository.BaseRepository[entity.ProjectFlock] GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) + SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) + SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) + SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) + SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) + SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) + GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) } type ClosingRepositoryImpl struct { @@ -102,6 +109,163 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak return rows, totalResults, nil } +func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, nil + } + + var purchaseAgg struct { + TotalIn float64 `gorm:"column:total_in"` + } + + err := r.DB().WithContext(ctx). + Table("purchase_items pi"). + Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'"). + Where("f.name = ?", "PAKAN"). + Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(pi.total_qty), 0) AS total_in"). + Scan(&purchaseAgg).Error + if err != nil { + return 0, 0, err + } + + var usageAgg struct { + TotalUsed float64 `gorm:"column:total_used"` + } + + err = r.DB().WithContext(ctx). + Table("recording_stocks rs"). + Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", "PAKAN"). + Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used"). + Scan(&usageAgg).Error + if err != nil { + return 0, 0, err + } + + return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil +} + +func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var agg struct { + Total float64 `gorm:"column:total_culling"` + } + + err := r.DB().WithContext(ctx). + Table("recording_depletions rd"). + Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", utils.FlagAyamCulling). + Select("COALESCE(SUM(rd.qty), 0) AS total_culling"). + Scan(&agg).Error + if err != nil { + return 0, err + } + + return agg.Total, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, nil + } + + var agg struct { + TotalQty float64 `gorm:"column:total_qty"` + } + + err := r.DB().WithContext(ctx). + Table("recording_eggs re"). + Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(re.qty), 0) AS total_qty"). + Scan(&agg).Error + if err != nil { + return 0, err + } + + return agg.TotalQty, nil +} + +func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) { + if fcrID == 0 { + return []entity.FcrStandard{}, nil + } + + var standards []entity.FcrStandard + if err := r.DB().WithContext(ctx). + Where("fcr_id = ?", fcrID). + Order("weight ASC"). + Find(&standards).Error; err != nil { + return nil, err + } + + return standards, nil +} + const ( sapronakIncomingPurchasesSQL = ` SELECT diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index 4d142f44..6de2dc0b 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -25,4 +25,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) route.Get("/:projectFlockId", ctrl.GetClosingSummary) route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + route.Get("/:projectFlockId/data-produksi", ctrl.GetClosingDataProduksi) } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index cfc22948..f8957a99 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -3,6 +3,7 @@ package service import ( "context" "errors" + "math" "strconv" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" @@ -30,6 +31,7 @@ type ClosingService interface { GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) + GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) } @@ -379,3 +381,219 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove return &result, nil } + +func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) { + if projectFlockID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + if err != nil { + s.Log.Errorf("Failed get project flock %d for closing data produksi: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + var population float64 + for _, history := range project.KandangHistory { + for _, chickin := range history.Chickins { + population += chickin.UsageQty + chickin.PendingUsageQty + } + } + + projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") + } + + feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) + if err != nil { + s.Log.Errorf("Failed to sum feed purchase/used qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data") + } + + claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) + if err != nil { + s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch claim culling data") + } + + finalPopulation := population - claimCulling + + var standards []entity.FcrStandard + if project.FcrId > 0 { + standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId) + if err != nil { + s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") + } + } + // masih dummy, karena tab penjualan agenya masih dummy juga + age := 1.0 + + feedUsedPerHead := 0.0 + if population > 0 { + feedUsedPerHead = feedUsed / population + } + + purchase := dto.ClosingPurchaseDTO{ + InitialPopulation: int(population), + ClaimCulling: int(claimCulling), + FinalPopulation: int(finalPopulation), + FeedIn: feedIn, + FeedUsed: feedUsed, + FeedUsedPerHead: feedUsedPerHead, + } + + chickenFlagNames := []string{string(utils.FlagPullet)} + chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chicken sales data") + } + + var chickenAverageWeight float64 + if chickenSalesQty > 0 { + chickenAverageWeight = chickenSalesWeight / chickenSalesQty + } + + var chickenAverageSellingPrice float64 + if chickenSalesWeight > 0 { + chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight + } + + eggFlagNames := []string{ + string(utils.FlagTelur), + string(utils.FlagTelurUtuh), + string(utils.FlagTelurPecah), + string(utils.FlagTelurPutih), + string(utils.FlagTelurRetak), + } + eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") + } + + var averageEggWeight float64 + if eggSalesQty > 0 { + averageEggWeight = eggSalesWeight / eggSalesQty + } + + var averageEggSellingPrice float64 + if eggSalesWeight > 0 { + averageEggSellingPrice = eggSalesPrice / eggSalesWeight + } + + chickenSales := dto.ClosingSalesDTO{ + SalesPopulation: int(chickenSalesQty), + SalesWeight: chickenSalesWeight, + AverageWeight: chickenAverageWeight, + AverageSellingPrice: chickenAverageSellingPrice, + } + + eggSales := dto.ClosingEggSalesDTO{ + EggPieces: int(eggSalesQty), + EggMassKg: eggSalesWeight, + AverageEggWeightKg: averageEggWeight, + AverageSellingPrice: averageEggSellingPrice, + } + + harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") + } + + chickenDepletion := population - chickenSalesQty + if chickenDepletion < 0 { + chickenDepletion = 0 + } + eggDepletion := harvestEggQty - eggSalesQty + if eggDepletion < 0 { + eggDepletion = 0 + } + + chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) + eggPerformance := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + + sales := dto.ClosingSalesGroupDTO{ + ChickenProduction: chickenSales, + EggProduction: eggSales, + } + + performance := dto.ClosingPerformanceDTO{ + Depletion: chickenPerformance.Depletion, + Age: age, + MortalityStd: chickenPerformance.MortalityStd, + MortalityAct: chickenPerformance.MortalityAct, + DeffMortality: chickenPerformance.DeffMortality, + FcrStd: eggPerformance.FcrStd, + FcrAct: eggPerformance.FcrAct, + DeffFcr: eggPerformance.DeffFcr, + Adg: eggPerformance.Adg, + } + + result := dto.ClosingProductionReportDTO{ + Purchase: purchase, + Sales: sales, + Performance: performance, + } + + return &result, nil +} + +func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { + mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) + + fcrAct := 0.0 + if totalWeight > 0 { + fcrAct = feedUsed / totalWeight + } + + mortalityAct := 0.0 + if basePopulation > 0 { + mortalityAct = (depletion / basePopulation) * 100 + } + + deffMortality := mortalityStd - mortalityAct + deffFcr := fcrStd - fcrAct + + adg := 0.0 + if age > 0 { + adg = averageWeight / age + } + + return dto.ClosingPerformanceDTO{ + Depletion: depletion, + Age: age, + MortalityStd: mortalityStd, + MortalityAct: mortalityAct, + DeffMortality: deffMortality, + FcrStd: fcrStd, + FcrAct: fcrAct, + DeffFcr: deffFcr, + Adg: adg, + } +} + +func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) { + if len(standards) == 0 || averageWeight <= 0 { + return 0, 0 + } + + closest := standards[0] + minDiff := math.Abs(closest.Weight - averageWeight) + for _, std := range standards[1:] { + diff := math.Abs(std.Weight - averageWeight) + if diff < minDiff { + minDiff = diff + closest = std + } + } + + return closest.Mortality, closest.FcrNumber +} From 047162699e385a295be9aca1417b18557956d2a9 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Thu, 18 Dec 2025 15:25:15 +0700 Subject: [PATCH 6/9] adjust response api closing data produksi --- internal/modules/closings/dto/closing.dto.go | 12 +- .../closings/services/closing.service.go | 120 ++++++++++-------- 2 files changed, 75 insertions(+), 57 deletions(-) diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index b3075776..429495b7 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -71,31 +71,31 @@ type ClosingSalesDTO struct { SalesPopulation int `json:"sales_population"` SalesWeight float64 `json:"sales_weight"` AverageWeight float64 `json:"average_weight"` - AverageSellingPrice float64 `json:"average_selling_price"` + AverageSellingPrice float64 `json:"chicken_average_selling_price"` } type ClosingEggSalesDTO struct { EggPieces int `json:"egg_pieces"` EggMassKg float64 `json:"egg_mass_kg"` AverageEggWeightKg float64 `json:"average_egg_weight_kg"` - AverageSellingPrice float64 `json:"average_selling_price"` + AverageSellingPrice float64 `json:"egg_average_selling_price"` } type ClosingPerformanceDTO struct { Depletion float64 `json:"depletion"` - Age float64 `json:"age"` + Age float64 `json:"age_day"` MortalityStd float64 `json:"mortality_std"` MortalityAct float64 `json:"mortality_act"` DeffMortality float64 `json:"deff_mortality"` FcrStd float64 `json:"fcr_std"` FcrAct float64 `json:"fcr_act"` DeffFcr float64 `json:"deff_fcr"` - Adg float64 `json:"adg"` + Awg float64 `json:"awg"` } type ClosingSalesGroupDTO struct { - ChickenProduction ClosingSalesDTO `json:"chicken_production"` - EggProduction ClosingEggSalesDTO `json:"egg_production"` + Chicken ClosingSalesDTO `json:"chicken"` + Egg *ClosingEggSalesDTO `json:"egg,omitempty"` } type ClosingProductionReportDTO struct { diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index f8957a99..e5479f35 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -5,6 +5,7 @@ import ( "errors" "math" "strconv" + "strings" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -403,6 +404,8 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint } } + isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) + projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) @@ -431,7 +434,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") } } - // masih dummy, karena tab penjualan agenya masih dummy juga + // masih dummy, karena tab penjualan agenya masih dummy age := 1.0 feedUsedPerHead := 0.0 @@ -465,29 +468,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight } - eggFlagNames := []string{ - string(utils.FlagTelur), - string(utils.FlagTelurUtuh), - string(utils.FlagTelurPecah), - string(utils.FlagTelurPutih), - string(utils.FlagTelurRetak), - } - eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) - if err != nil { - s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") - } - - var averageEggWeight float64 - if eggSalesQty > 0 { - averageEggWeight = eggSalesWeight / eggSalesQty - } - - var averageEggSellingPrice float64 - if eggSalesWeight > 0 { - averageEggSellingPrice = eggSalesPrice / eggSalesWeight - } - chickenSales := dto.ClosingSalesDTO{ SalesPopulation: int(chickenSalesQty), SalesWeight: chickenSalesWeight, @@ -495,34 +475,65 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint AverageSellingPrice: chickenAverageSellingPrice, } - eggSales := dto.ClosingEggSalesDTO{ - EggPieces: int(eggSalesQty), - EggMassKg: eggSalesWeight, - AverageEggWeightKg: averageEggWeight, - AverageSellingPrice: averageEggSellingPrice, - } - - harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) - if err != nil { - s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") - } - chickenDepletion := population - chickenSalesQty if chickenDepletion < 0 { chickenDepletion = 0 } - eggDepletion := harvestEggQty - eggSalesQty - if eggDepletion < 0 { - eggDepletion = 0 - } chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) - eggPerformance := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + + var eggSales *dto.ClosingEggSalesDTO + var eggPerformance *dto.ClosingPerformanceDTO + if !isGrowing { + eggFlagNames := []string{ + string(utils.FlagTelur), + string(utils.FlagTelurUtuh), + string(utils.FlagTelurPecah), + string(utils.FlagTelurPutih), + string(utils.FlagTelurRetak), + } + + eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") + } + + var averageEggWeight float64 + if eggSalesQty > 0 { + averageEggWeight = eggSalesWeight / eggSalesQty + } + + var averageEggSellingPrice float64 + if eggSalesWeight > 0 { + averageEggSellingPrice = eggSalesPrice / eggSalesWeight + } + + eggSales = &dto.ClosingEggSalesDTO{ + EggPieces: int(eggSalesQty), + EggMassKg: eggSalesWeight, + AverageEggWeightKg: averageEggWeight, + AverageSellingPrice: averageEggSellingPrice, + } + + harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") + } + + eggDepletion := harvestEggQty - eggSalesQty + if eggDepletion < 0 { + eggDepletion = 0 + } + + eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + eggPerformance = &eggPerf + } sales := dto.ClosingSalesGroupDTO{ - ChickenProduction: chickenSales, - EggProduction: eggSales, + Chicken: chickenSales, + Egg: eggSales, } performance := dto.ClosingPerformanceDTO{ @@ -531,10 +542,17 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint MortalityStd: chickenPerformance.MortalityStd, MortalityAct: chickenPerformance.MortalityAct, DeffMortality: chickenPerformance.DeffMortality, - FcrStd: eggPerformance.FcrStd, - FcrAct: eggPerformance.FcrAct, - DeffFcr: eggPerformance.DeffFcr, - Adg: eggPerformance.Adg, + } + if eggPerformance != nil { + performance.FcrStd = eggPerformance.FcrStd + performance.FcrAct = eggPerformance.FcrAct + performance.DeffFcr = eggPerformance.DeffFcr + performance.Awg = eggPerformance.Awg + } else { + performance.FcrStd = chickenPerformance.FcrStd + performance.FcrAct = chickenPerformance.FcrAct + performance.DeffFcr = chickenPerformance.DeffFcr + performance.Awg = chickenPerformance.Awg } result := dto.ClosingProductionReportDTO{ @@ -562,9 +580,9 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul deffMortality := mortalityStd - mortalityAct deffFcr := fcrStd - fcrAct - adg := 0.0 + awg := 0.0 if age > 0 { - adg = averageWeight / age + awg = averageWeight / age } return dto.ClosingPerformanceDTO{ @@ -576,7 +594,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul FcrStd: fcrStd, FcrAct: fcrAct, DeffFcr: deffFcr, - Adg: adg, + Awg: awg, } } From f5c80fa560aafb6de4f5c22c3290e5e9d0e4fe00 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 18 Dec 2025 16:21:46 +0700 Subject: [PATCH 7/9] Feat(BE-339):Fixing dto reporting per supplier --- .../repports/dto/repportPurchase.dto.go | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/modules/repports/dto/repportPurchase.dto.go b/internal/modules/repports/dto/repportPurchase.dto.go index 60fd0fee..830a076f 100644 --- a/internal/modules/repports/dto/repportPurchase.dto.go +++ b/internal/modules/repports/dto/repportPurchase.dto.go @@ -1,6 +1,7 @@ package dto import ( + "math" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -26,10 +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"` + 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 { @@ -119,6 +122,11 @@ 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) @@ -127,6 +135,20 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem summary.TotalPurchaseValue += row.PurchaseValue 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 transportUnitPriceCount > 0 { + summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount)) } return PurchaseSupplierDTO{ @@ -135,4 +157,3 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem Summary: summary, } } - From cb076d92ace5a7e3eb666dd783a9a83573bfc6b6 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 18 Dec 2025 16:41:56 +0700 Subject: [PATCH 8/9] Feat(BE-339):Fixing dto reporting per supplier, and adjust limit --- internal/modules/repports/validations/repport.validation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 53ba22d7..a69e7716 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -30,7 +30,7 @@ type MarketingQuery struct { type PurchaseSupplierQuery 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"` AreaId int64 `query:"area_id" validate:"omitempty"` SupplierId int64 `query:"supplier_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"` From 207382b3b0f5842478cd9d52e4373a61873f3785 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Fri, 19 Dec 2025 07:05:11 +0700 Subject: [PATCH 9/9] fix get all inventory product stock --- .../product-stocks/services/product-stock.service.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index a0765d84..11475109 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -64,11 +64,18 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e offset := (params.Page - 1) * params.Limit productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Where(`EXISTS ( + SELECT 1 + FROM product_warehouses pw + WHERE pw.product_id = products.id + AND pw.qty > 0 + )`) + db = s.withRelations(db) if params.Search != "" { - return db.Where("name ILIKE ?", "%"+params.Search+"%") + db = db.Where("products.name ILIKE ?", "%"+params.Search+"%") } - return db.Order("created_at DESC").Order("updated_at DESC") + return db.Order("products.created_at DESC").Order("products.updated_at DESC") }) if err != nil {