From f59cdd821ab9d71d2f2224be5e0e41395db4209c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 3 Feb 2026 13:32:37 +0700 Subject: [PATCH 01/12] FEAT[BE] :add marketing type and conversion fields to marketing entities and services --- ...458_create_transfer_laying_sequence.up.sql | 2 +- ...203054206_update_marketing_tables.down.sql | 8 ++ ...60203054206_update_marketing_tables.up.sql | 9 ++ internal/entities/marketing.go | 1 + internal/entities/marketing_product.go | 19 +-- .../services/deliveryorder.service.go | 40 +++---- .../marketing/services/salesorder.service.go | 108 ++++++++---------- .../validations/salesorder.validation.go | 15 ++- internal/utils/constant.go | 40 +++++++ 9 files changed, 141 insertions(+), 101 deletions(-) create mode 100644 internal/database/migrations/20260203054206_update_marketing_tables.down.sql create mode 100644 internal/database/migrations/20260203054206_update_marketing_tables.up.sql diff --git a/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql index f5f5bdf7..1a48a512 100644 --- a/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql +++ b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql @@ -1,5 +1,5 @@ -- Create sequence for transfer laying movement number -CREATE SEQUENCE transfer_laying_seq START +CREATE SEQUENCE IF NOT EXISTS transfer_laying_seq START WITH 1 INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999 NO CYCLE; diff --git a/internal/database/migrations/20260203054206_update_marketing_tables.down.sql b/internal/database/migrations/20260203054206_update_marketing_tables.down.sql new file mode 100644 index 00000000..b498f23e --- /dev/null +++ b/internal/database/migrations/20260203054206_update_marketing_tables.down.sql @@ -0,0 +1,8 @@ +-- Remove columns from marketing_products +ALTER TABLE marketing_products +DROP COLUMN IF EXISTS week, +DROP COLUMN IF EXISTS weight_per_convertion, +DROP COLUMN IF EXISTS convertion_unit; + +-- Remove column from marketings +ALTER TABLE marketings DROP COLUMN IF EXISTS marketing_type; \ No newline at end of file diff --git a/internal/database/migrations/20260203054206_update_marketing_tables.up.sql b/internal/database/migrations/20260203054206_update_marketing_tables.up.sql new file mode 100644 index 00000000..72f7c8e7 --- /dev/null +++ b/internal/database/migrations/20260203054206_update_marketing_tables.up.sql @@ -0,0 +1,9 @@ +-- Add marketing_type to marketings table +ALTER TABLE marketings +ADD COLUMN IF NOT EXISTS marketing_type VARCHAR(50); + +-- Add convertion fields to marketing_products table +ALTER TABLE marketing_products +ADD COLUMN IF NOT EXISTS convertion_unit VARCHAR(20), +ADD COLUMN IF NOT EXISTS weight_per_convertion NUMERIC(15, 3), +ADD COLUMN IF NOT EXISTS week INTEGER; \ No newline at end of file diff --git a/internal/entities/marketing.go b/internal/entities/marketing.go index c9ff7624..c1ca293b 100644 --- a/internal/entities/marketing.go +++ b/internal/entities/marketing.go @@ -14,6 +14,7 @@ type Marketing struct { SoDate time.Time `gorm:"type:date;not null"` SalesPersonId uint `gorm:"not null"` Notes string `gorm:"type:text"` + MarketingType string `gorm:"type:varchar(50)"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index f2294f10..ce13d3d8 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -1,14 +1,17 @@ package entities type MarketingProduct struct { - Id uint `gorm:"primaryKey;autoIncrement"` - MarketingId uint `gorm:"not null"` - ProductWarehouseId uint `gorm:"not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - AvgWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + ConvertionUnit *string `gorm:"type:varchar(20)"` + WeightPerConvertion *float64 `gorm:"type:numeric(15,3)"` + Week *int `gorm:"type:integer"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 6d9392a6..80045027 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -237,6 +237,12 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction) + + marketing, err := marketingRepoTx.GetByID(c.Context(), req.MarketingId, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) if err != nil { @@ -283,23 +289,11 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery itemDeliveryDate = &parsedDate } - isPakanOrOVK := false - if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { - for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight var totalPrice float64 - if isPakanOrOVK { - + if marketing.MarketingType == string(utils.MarketingTypeTrading) { totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice } else { - totalPrice = totalWeight * requestedProduct.UnitPrice } @@ -374,6 +368,12 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) + marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction) + + marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -421,23 +421,11 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO itemDeliveryDate = deliveryProduct.DeliveryDate } - isPakanOrOVK := false - if foundMarketingProduct.ProductWarehouse.Id != 0 && foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { - for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight var totalPrice float64 - if isPakanOrOVK { - + if marketing.MarketingType == string(utils.MarketingTypeTrading) { totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice } else { - totalPrice = totalWeight * requestedProduct.UnitPrice } diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index df75fe82..a43370d5 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -103,6 +103,10 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, err } + if !utils.IsValidMarketingType(req.MarketingType) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM PULLET") + } + actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err @@ -115,6 +119,9 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } for _, item := range req.MarketingProducts { + if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") + } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err } @@ -149,6 +156,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e SoDate: soDate, SalesPersonId: req.SalesPersonId, Notes: req.Notes, + MarketingType: req.MarketingType, CreatedBy: actorID, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { @@ -161,10 +169,9 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e if product.ProductWarehouseId != 0 { pwIDs = append(pwIDs, product.ProductWarehouseId) } - if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, marketing.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } - } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil { return err @@ -207,6 +214,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, err } + if req.MarketingType != "" && !utils.IsValidMarketingType(req.MarketingType) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM PULLET") + } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { return nil, err } @@ -234,6 +245,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if len(req.MarketingProducts) > 0 { for _, item := range req.MarketingProducts { + if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") + } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err } @@ -281,6 +295,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if req.Notes != "" { updateBody["notes"] = req.Notes } + if req.MarketingType != "" { + updateBody["marketing_type"] = req.MarketingType + } if len(updateBody) > 0 { if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { @@ -306,31 +323,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u reqByPW[rp.ProductWarehouseId] = rp } + marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } + for _, rp := range req.MarketingProducts { if old, ok := oldByPW[rp.ProductWarehouseId]; ok { - // Get product untuk cek flag PAKAN atau OVK - productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags") - }) - if err != nil { - return err - } - - // Cek apakah product punya flag PAKAN atau OVK - isPakanOrOVK := false - if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 { - for _, flag := range productWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } - totalWeight := rp.Qty * rp.AvgWeight var totalPrice float64 - if isPakanOrOVK { + if marketing.MarketingType == string(utils.MarketingTypeTrading) { totalPrice = rp.Qty * rp.UnitPrice } else { totalPrice = totalWeight * rp.UnitPrice @@ -340,7 +343,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") } - if err == nil && deliveryProduct.Id != 0 { oldQty := old.Qty newQty := rp.Qty @@ -363,12 +365,15 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } updateBody := map[string]any{ - "product_warehouse_id": rp.ProductWarehouseId, - "qty": rp.Qty, - "unit_price": rp.UnitPrice, - "avg_weight": rp.AvgWeight, - "total_weight": totalWeight, - "total_price": totalPrice, + "product_warehouse_id": rp.ProductWarehouseId, + "qty": rp.Qty, + "unit_price": rp.UnitPrice, + "avg_weight": rp.AvgWeight, + "total_weight": totalWeight, + "total_price": totalPrice, + "convertion_unit": rp.ConvertionUnit, + "weight_per_convertion": rp.WeightPerConvertion, + "week": rp.Week, } if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") @@ -391,7 +396,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } } else { - if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + if err := s.createMarketingProductWithDelivery(c.Context(), id, marketing.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } @@ -399,7 +404,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u for _, old := range oldProducts { if _, ok := reqByPW[old.ProductWarehouseId]; !ok { - deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") @@ -682,45 +686,27 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e return updated, nil } -func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { - - // Get product untuk cek flag PAKAN atau OVK - productWarehouse, err := s.ProductWarehouseRepo.GetByID(ctx, rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags") - }) - if err != nil { - return err - } - - // Cek apakah product punya flag PAKAN atau OVK - isPakanOrOVK := false - if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 { - for _, flag := range productWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } +func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, marketingType string, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { totalWeight := rp.Qty * rp.AvgWeight var totalPrice float64 - if isPakanOrOVK { - // PAKAN atau OVK: qty × unit_price + if marketingType == string(utils.MarketingTypeTrading) { totalPrice = rp.Qty * rp.UnitPrice } else { - // Produk lain: total_weight × unit_price totalPrice = totalWeight * rp.UnitPrice } marketingProduct := &entity.MarketingProduct{ - MarketingId: marketingId, - ProductWarehouseId: rp.ProductWarehouseId, - Qty: rp.Qty, - UnitPrice: rp.UnitPrice, - AvgWeight: rp.AvgWeight, - TotalWeight: totalWeight, - TotalPrice: totalPrice, + MarketingId: marketingId, + ProductWarehouseId: rp.ProductWarehouseId, + Qty: rp.Qty, + UnitPrice: rp.UnitPrice, + AvgWeight: rp.AvgWeight, + TotalWeight: totalWeight, + TotalPrice: totalPrice, + ConvertionUnit: rp.ConvertionUnit, + WeightPerConvertion: rp.WeightPerConvertion, + Week: rp.Week, } if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { return err diff --git a/internal/modules/marketing/validations/salesorder.validation.go b/internal/modules/marketing/validations/salesorder.validation.go index b69da394..9a3cee29 100644 --- a/internal/modules/marketing/validations/salesorder.validation.go +++ b/internal/modules/marketing/validations/salesorder.validation.go @@ -5,15 +5,19 @@ type Create struct { SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"` Date string `json:"date" validate:"required,datetime=2006-01-02"` Notes string `json:"notes" validate:"omitempty,max=500"` + MarketingType string `json:"marketing_type" validate:"required,min=1,max=50"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"` } type CreateMarketingProduct struct { - VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` - ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` - UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` - Qty float64 `json:"qty" validate:"required,gt=0"` - AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` + ConvertionUnit *string `json:"convertion_unit" validate:"omitempty,min=1,max=20"` + WeightPerConvertion *float64 `json:"weight_per_convertion" validate:"omitempty,gt=0"` + Week *int `json:"week" validate:"omitempty,gt=0"` + ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` } type Update struct { @@ -21,6 +25,7 @@ type Update struct { SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"` Date string `json:"date" validate:"omitempty,datetime=2006-01-02"` Notes string `json:"notes" validate:"omitempty,max=500"` + MarketingType string `json:"marketing_type" validate:"omitempty,min=1,max=50"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 9abd6a30..cb8a0ba2 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -212,6 +212,30 @@ const ( KandangStatusActive KandangStatus = "ACTIVE" ) +// ------------------------------------------------------------------- +// Marketing Type +// ------------------------------------------------------------------- + +type MarketingType string + +const ( + MarketingTypeAyam MarketingType = "AYAM" + MarketingTypeTelur MarketingType = "TELUR" + MarketingTypeTrading MarketingType = "TRADING" + MarketingTypeAyamPullet MarketingType = "AYAM PULLET" +) + +// ------------------------------------------------------------------- +// Convertion Unit +// ------------------------------------------------------------------- + +type ConvertionUnit string + +const ( + ConvertionUnitPeti ConvertionUnit = "PETI" + ConvertionUnitKG ConvertionUnit = "KG" +) + // ------------------------------------------------------------------- // ProjectFlockCategory // ------------------------------------------------------------------- @@ -609,6 +633,22 @@ func IsValidPaymentParty(v string) bool { return false } +func IsValidMarketingType(v string) bool { + switch MarketingType(v) { + case MarketingTypeAyam, MarketingTypeTelur, MarketingTypeTrading, MarketingTypeAyamPullet: + return true + } + return false +} + +func IsValidConvertionUnit(v string) bool { + switch ConvertionUnit(v) { + case ConvertionUnitPeti, ConvertionUnitKG: + return true + } + return false +} + // example use // Recording helper From b862fc41133130597ba716ca0f1dc06b71cb42bb Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 3 Feb 2026 17:01:50 +0700 Subject: [PATCH 02/12] [FEAT/BE]Fix remove fcr master data and changes to standart production --- ...63959_remove_fcr_id_project_flock.down.sql | 47 +++++++++++++++++++ ...3063959_remove_fcr_id_project_flock.up.sql | 26 ++++++++++ internal/entities/projectflock.go | 2 - .../repositories/closing.repository.go | 17 ------- .../closings/services/closing.service.go | 36 ++------------ .../dashboard_stats.repository.go | 24 ---------- .../production/chickins/dto/chickin.dto.go | 31 +++++++----- .../chickins/services/chickin.service.go | 2 +- .../dto/project_flock_kandang.dto.go | 5 +- .../project_flocks/dto/projectflock.dto.go | 27 +++++++---- .../dto/projectflock_kandang.dto.go | 8 +--- .../repositories/projectflock.repository.go | 10 +--- .../projectflock_kandang.repository.go | 8 ++-- .../services/projectflock.service.go | 2 - .../validations/projectflock.validation.go | 1 - .../recordings/dto/recording.dto.go | 6 +-- .../repositories/recording.repository.go | 31 +----------- .../repports/services/repport.service.go | 31 ++++++++++-- 18 files changed, 155 insertions(+), 159 deletions(-) create mode 100644 internal/database/migrations/20260203063959_remove_fcr_id_project_flock.down.sql create mode 100644 internal/database/migrations/20260203063959_remove_fcr_id_project_flock.up.sql diff --git a/internal/database/migrations/20260203063959_remove_fcr_id_project_flock.down.sql b/internal/database/migrations/20260203063959_remove_fcr_id_project_flock.down.sql new file mode 100644 index 00000000..42991241 --- /dev/null +++ b/internal/database/migrations/20260203063959_remove_fcr_id_project_flock.down.sql @@ -0,0 +1,47 @@ +BEGIN; + +DO $$ +DECLARE + fallback_fcr_id BIGINT; +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_flocks' + AND column_name = 'fcr_id' + ) THEN + ALTER TABLE project_flocks + ADD COLUMN fcr_id BIGINT; + END IF; + + SELECT id INTO fallback_fcr_id + FROM fcrs + ORDER BY id ASC + LIMIT 1; + + IF fallback_fcr_id IS NOT NULL THEN + UPDATE project_flocks + SET fcr_id = fallback_fcr_id + WHERE fcr_id IS NULL; + + ALTER TABLE project_flocks + ALTER COLUMN fcr_id SET NOT NULL; + END IF; + + IF EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'project_flocks_fcr_id_fkey' + ) THEN + ALTER TABLE project_flocks + DROP CONSTRAINT project_flocks_fcr_id_fkey; + END IF; + + ALTER TABLE project_flocks + ADD CONSTRAINT project_flocks_fcr_id_fkey + FOREIGN KEY (fcr_id) REFERENCES fcrs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; +END $$; + +COMMIT; diff --git a/internal/database/migrations/20260203063959_remove_fcr_id_project_flock.up.sql b/internal/database/migrations/20260203063959_remove_fcr_id_project_flock.up.sql new file mode 100644 index 00000000..e34e7d92 --- /dev/null +++ b/internal/database/migrations/20260203063959_remove_fcr_id_project_flock.up.sql @@ -0,0 +1,26 @@ +BEGIN; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_flocks' + AND column_name = 'fcr_id' + ) THEN + IF EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'project_flocks_fcr_id_fkey' + ) THEN + ALTER TABLE project_flocks + DROP CONSTRAINT project_flocks_fcr_id_fkey; + END IF; + + ALTER TABLE project_flocks + DROP COLUMN fcr_id; + END IF; +END $$; + +COMMIT; diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go index 7243c9c4..80d7f886 100644 --- a/internal/entities/projectflock.go +++ b/internal/entities/projectflock.go @@ -11,7 +11,6 @@ type ProjectFlock struct { FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"` AreaId uint `gorm:"not null"` Category string `gorm:"type:varchar(20);not null"` - FcrId uint `gorm:"not null"` ProductionStandardId uint `gorm:"column:production_standard_id"` LocationId uint `gorm:"not null"` CreatedBy uint `gorm:"not null"` @@ -20,7 +19,6 @@ type ProjectFlock struct { DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` Area Area `gorm:"foreignKey:AreaId;references:Id"` - Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index cd5ce2da..6ec09858 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -24,7 +24,6 @@ type ClosingRepository interface { 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) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) @@ -393,22 +392,6 @@ func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFla 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 -} - func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) { db := r.DB().WithContext(ctx) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 923a2b1c..71bfcdec 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -836,14 +836,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint 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") - } - } age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID, kandangID) if err != nil { s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err) @@ -893,7 +885,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint chickenDepletion = 0 } - chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) +chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age) if fcrActFromRecording != nil { chickenPerformance.FcrAct = *fcrActFromRecording } @@ -943,7 +935,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint eggDepletion = 0 } - eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age) if fcrActFromRecording != nil { eggPerf.FcrAct = *fcrActFromRecording } @@ -1001,10 +993,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint performance.EggMass = eggMass } } - performance.DeffFcr = performance.FcrStd - performance.FcrAct if productionStandardDetail != nil { if productionStandardDetail.StandardFCR != nil { performance.FcrStd = *productionStandardDetail.StandardFCR + performance.DeffFcr = performance.FcrStd - performance.FcrAct } if !isGrowing { if productionStandardDetail.TargetHenDayProduction != nil { @@ -1091,8 +1083,8 @@ func (s closingService) determineProductionWeek(ctx context.Context, projectFloc return week, nil } -func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { - mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) +func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64) dto.ClosingPerformanceDTO { + mortalityStd, fcrStd := 0.0, 0.0 fcrAct := 0.0 if totalWeight > 0 { @@ -1124,21 +1116,3 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul AwgAct: awg, } } - -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 -} diff --git a/internal/modules/dashboards/repositories/dashboard_stats.repository.go b/internal/modules/dashboards/repositories/dashboard_stats.repository.go index 3c04f9a0..363e6aa5 100644 --- a/internal/modules/dashboards/repositories/dashboard_stats.repository.go +++ b/internal/modules/dashboards/repositories/dashboard_stats.repository.go @@ -444,30 +444,6 @@ func (r *DashboardRepositoryImpl) standardIDSubquery(filters *validation.Dashboa return db } -func (r *DashboardRepositoryImpl) standardSourceSubquery(filters *validation.DashboardFilter) *gorm.DB { - db := r.DB(). - Table("project_flocks AS pf"). - Select("DISTINCT pf.production_standard_id, pf.fcr_id"). - Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id"). - Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). - Where("pf.production_standard_id > 0"). - Where("pf.fcr_id > 0") - - if filters != nil { - if len(filters.FlockIds) > 0 { - db = db.Where("pf.id IN ?", filters.FlockIds) - } - if len(filters.KandangIds) > 0 { - db = db.Where("k.id IN ?", filters.KandangIds) - } - if len(filters.LokasiIds) > 0 { - db = db.Where("k.location_id IN ?", filters.LokasiIds) - } - } - - return db -} - func (r *DashboardRepositoryImpl) GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error) { seriesExpr, labelExpr, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType) if err != nil { diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index d53b9491..8a4b0d09 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -5,7 +5,6 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" areaRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" - fcrRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" flockRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" kandangRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" @@ -13,6 +12,7 @@ import ( warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" userRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) // === DTO Structs (ordered) === @@ -40,7 +40,7 @@ type ProjectFlockDTO struct { Category string `json:"category"` Flock *flockRelationDTO.FlockRelationDTO `json:"flock"` Area *areaRelationDTO.AreaRelationDTO `json:"area"` - Fcr *fcrRelationDTO.FcrRelationDTO `json:"fcr"` + StandardFcr *float64 `json:"standard_fcr"` Location *locationRelationDTO.LocationRelationDTO `json:"location"` } @@ -97,10 +97,6 @@ func ToAreaDTO(e entity.Area) areaRelationDTO.AreaRelationDTO { return areaRelationDTO.ToAreaRelationDTO(e) } -func ToFcrDTO(e entity.Fcr) fcrRelationDTO.FcrRelationDTO { - return fcrRelationDTO.ToFcrRelationDTO(e) -} - func ToLocationDTO(e entity.Location) locationRelationDTO.LocationRelationDTO { return locationRelationDTO.ToLocationRelationDTO(e) } @@ -121,11 +117,6 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO { mapped := areaRelationDTO.ToAreaRelationDTO(e.Area) area = &mapped } - var fcr *fcrRelationDTO.FcrRelationDTO - if e.Fcr.Id != 0 { - mapped := fcrRelationDTO.ToFcrRelationDTO(e.Fcr) - fcr = &mapped - } var location *locationRelationDTO.LocationRelationDTO if e.Location.Id != 0 { mapped := locationRelationDTO.ToLocationRelationDTO(e.Location) @@ -137,7 +128,7 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO { Category: e.Category, Flock: flock, Area: area, - Fcr: fcr, + StandardFcr: resolveProjectFlockStandardFcr(e), Location: location, } } @@ -222,6 +213,22 @@ func ToChickinListDTOs(e []entity.ProjectChickin) []ChickinListDTO { return result } +func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 { + if e.ProductionStandard.Id == 0 || len(e.ProductionStandard.ProductionStandardDetails) == 0 { + return nil + } + week := 1 + if e.Category == string(utils.ProjectFlockCategoryLaying) { + week = 18 + } + for _, detail := range e.ProductionStandard.ProductionStandardDetails { + if detail.Week == week && detail.StandardFCR != nil { + return detail.StandardFCR + } + } + return nil +} + func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { result := make([]ChickinSimpleDTO, len(e)) for i, r := range e { diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 971ee072..a011c579 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -81,7 +81,7 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock.Area"). - Preload("ProjectFlockKandang.ProjectFlock.Fcr"). + Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard.ProductionStandardDetails"). Preload("ProjectFlockKandang.ProjectFlock.Location"). Preload("ProjectFlockKandang.ProjectFlock.Location.Area") diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index c8faf761..8231a551 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -8,7 +8,6 @@ import ( approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" - fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto" @@ -31,7 +30,7 @@ type ProjectFlockDTO struct { projectFlockDTO.ProjectFlockRelationDTO Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Category string `json:"category"` - Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + StandardFcr *float64 `json:"standard_fcr,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` @@ -86,7 +85,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO ProjectFlockRelationDTO: pf.ProjectFlockRelationDTO, Area: pf.Area, Category: pf.Category, - Fcr: pf.Fcr, + StandardFcr: pf.StandardFcr, ProductionStandard: pf.ProductionStandard, Location: pf.Location, CreatedUser: pf.CreatedUser, diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index 504d439c..e7240b49 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -6,7 +6,6 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" - fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto" @@ -28,7 +27,7 @@ type ProjectFlockListDTO struct { ProjectFlockRelationDTO Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Category string `json:"category"` - Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + StandardFcr *float64 `json:"standard_fcr,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` @@ -99,12 +98,6 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF areaSummary = &mapped } - var fcrSummary *fcrDTO.FcrRelationDTO - if e.Fcr.Id != 0 { - mapped := fcrDTO.ToFcrRelationDTO(e.Fcr) - fcrSummary = &mapped - } - var productionStandardSummary *productionStandardDTO.ProductionStandardRelationDTO if e.ProductionStandard.Id != 0 { mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProductionStandard) @@ -129,7 +122,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF Kandangs: kandangSummaries, ProjectBudgets: ToProjectBudgetDTOs(e.Budgets), Category: e.Category, - Fcr: fcrSummary, + StandardFcr: resolveProjectFlockStandardFcr(e), ProductionStandard: productionStandardSummary, Location: locationSummary, CreatedAt: e.CreatedAt, @@ -204,6 +197,22 @@ func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlo } } +func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 { + if e.ProductionStandard.Id == 0 || len(e.ProductionStandard.ProductionStandardDetails) == 0 { + return nil + } + week := 1 + if e.Category == string(utils.ProjectFlockCategoryLaying) { + week = 18 + } + for _, detail := range e.ProductionStandard.ProductionStandardDetails { + if detail.Week == week && detail.StandardFCR != nil { + return detail.StandardFCR + } + } + return nil +} + func ToProjectBudgetDTO(e entity.ProjectBudget) ProjectBudgetDTO { var nonstockRef *nonstockDTO.NonstockRelationDTO if e.Nonstock != nil && e.Nonstock.Id != 0 { diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 39abfe62..5c055a1d 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -5,7 +5,6 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" - fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto" @@ -22,7 +21,7 @@ type ProjectFlockWithPivotDTO struct { ProjectFlockRelationDTO Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Category string `json:"category"` - Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + StandardFcr *float64 `json:"standard_fcr,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` ProductionStandardId uint `json:"production_standard_id"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` @@ -67,10 +66,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD mapped := areaDTO.ToAreaRelationDTO(e.ProjectFlock.Area) pfLocal.Area = &mapped } - if e.ProjectFlock.Fcr.Id != 0 { - mapped := fcrDTO.ToFcrRelationDTO(e.ProjectFlock.Fcr) - pfLocal.Fcr = &mapped - } if e.ProjectFlock.ProductionStandard.Id != 0 { mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProjectFlock.ProductionStandard) pfLocal.ProductionStandard = &mapped @@ -83,6 +78,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD mapped := userDTO.ToUserRelationDTO(e.ProjectFlock.CreatedUser) pfLocal.CreatedUser = &mapped } + pfLocal.StandardFcr = resolveProjectFlockStandardFcr(e.ProjectFlock) for _, k := range e.ProjectFlock.Kandangs { kb := kandangDTO.ToKandangRelationDTO(k) diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 346f2176..cd7aaba7 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -23,7 +23,6 @@ type ProjectflockRepository interface { GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error) IdExists(ctx context.Context, id uint) (bool, error) AreaExists(ctx context.Context, id uint) (bool, error) - FcrExists(ctx context.Context, id uint) (bool, error) ProductionStandardExists(ctx context.Context, id uint) (bool, error) LocationExists(ctx context.Context, id uint) (bool, error) } @@ -67,8 +66,8 @@ func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm return db. Preload("CreatedUser"). Preload("Area"). - Preload("Fcr"). Preload("ProductionStandard"). + Preload("ProductionStandard.ProductionStandardDetails"). Preload("Location"). Preload("Kandangs"). Preload("KandangHistory"). @@ -134,14 +133,12 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s likeQuery := "%" + normalized + "%" return db. Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id"). - Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id"). Joins("LEFT JOIN production_standards ON production_standards.id = project_flocks.production_standard_id"). Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id"). Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by"). Where(` LOWER(areas.name) LIKE ? OR LOWER(project_flocks.category) LIKE ? - OR LOWER(fcrs.name) LIKE ? OR LOWER(production_standards.name) LIKE ? OR LOWER(locations.name) LIKE ? OR LOWER(locations.address) LIKE ? @@ -172,7 +169,6 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s likeQuery, likeQuery, likeQuery, - likeQuery, ) } @@ -184,10 +180,6 @@ func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (b return repository.Exists[entity.Area](ctx, r.DB(), id) } -func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bool, error) { - return repository.Exists[entity.Fcr](ctx, r.DB(), id) -} - func (r *ProjectflockRepositoryImpl) ProductionStandardExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.ProductionStandard](ctx, r.DB(), id) } diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 9f5bb0e2..002c2c58 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -117,10 +117,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Area"). Preload("ProjectFlock.Location"). Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails"). Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). @@ -208,10 +208,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context. Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Area"). Preload("ProjectFlock.Location"). Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails"). Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). @@ -324,10 +324,10 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint record := new(entity.ProjectFlockKandang) if err := r.db.WithContext(ctx). Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Area"). Preload("ProjectFlock.Location"). Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails"). Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). @@ -347,10 +347,10 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont if err := r.db.WithContext(ctx). Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID). Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). Preload("ProjectFlock.Area"). Preload("ProjectFlock.Location"). Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails"). Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 96e4b6b0..224f43bf 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -282,7 +282,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists}, - commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists}, commonSvc.RelationCheck{Name: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists}, ); err != nil { @@ -334,7 +333,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* createBody := &entity.ProjectFlock{ AreaId: req.AreaId, Category: cat, - FcrId: req.FcrId, ProductionStandardId: req.ProductionStandardId, LocationId: req.LocationId, CreatedBy: actorID, diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index 1fb48abe..ca347d47 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -4,7 +4,6 @@ type Create struct { FlockName string `json:"flock_name" validate:"required_strict"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` Category string `json:"category" validate:"required_strict"` - FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 191b9676..ec2f3657 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -280,10 +280,10 @@ func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO { } } - if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil { + if pfk.ProjectFlock.ProductionStandard.Id != 0 || e.StandardFcr != nil { result.Fcr = &RecordingFcrDTO{ - Id: pfk.ProjectFlock.Fcr.Id, - Name: pfk.ProjectFlock.Fcr.Name, + Id: pfk.ProjectFlock.ProductionStandard.Id, + Name: pfk.ProjectFlock.ProductionStandard.Name, FcrStd: floatValue(e.StandardFcr), } } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index ce4dc0df..b9867d2b 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -46,7 +46,6 @@ type RecordingRepository interface { GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) - GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) @@ -92,7 +91,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB { Preload("ProjectFlockKandang.Kandang.Location"). Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard"). - Preload("ProjectFlockKandang.ProjectFlock.Fcr"). + // Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("Depletions"). Preload("Depletions.ProductWarehouse"). Preload("Depletions.ProductWarehouse.Product"). @@ -448,34 +447,6 @@ func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang( Scan(&result).Error return result, err } - -func (r *RecordingRepositoryImpl) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) { - if fcrId == 0 || currentWeightKg <= 0 { - return 0, false, nil - } - - var standard entity.FcrStandard - err := tx. - Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg). - Order("weight ASC"). - First(&standard).Error - - if errors.Is(err, gorm.ErrRecordNotFound) { - err = tx. - Where("fcr_id = ?", fcrId). - Order("weight DESC"). - First(&standard).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, false, nil - } - } - if err != nil { - return 0, false, err - } - - return standard.FcrNumber, true, nil -} - func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { // Body-weight tracking is removed; keep stub for report compatibility. return 0, 0, nil diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index d45cba62..d417642e 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1352,10 +1352,12 @@ func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Loc poNumber = *purchase.PoNumber } - prDate := purchase.CreatedAt.In(loc) - startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc) + startDate := resolveDebtSupplierReceivedDate(purchase, loc) endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) - aging := int(endDate.Sub(startDate).Hours() / 24) + aging := 0 + if !startDate.IsZero() { + aging = int(endDate.Sub(startDate).Hours() / 24) + } totalPrice := 0.0 travelNumber := "-" @@ -1525,8 +1527,10 @@ func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool { } 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) + startDate := resolveDebtSupplierReceivedDate(purchase, loc) + if startDate.IsZero() { + return 0 + } stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc) if stopDate.Before(startDate) { return 0 @@ -1534,6 +1538,23 @@ func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc return int(stopDate.Sub(startDate).Hours() / 24) } +func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Location) time.Time { + earliest := time.Time{} + for _, item := range purchase.Items { + if item.ReceivedDate == nil || item.ReceivedDate.IsZero() { + continue + } + received := item.ReceivedDate.In(loc) + if earliest.IsZero() || received.Before(earliest) { + earliest = received + } + } + if earliest.IsZero() { + return time.Time{} + } + return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc) +} + func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) { params, filters, err := s.parseHppPerKandangQuery(ctx) if err != nil { From 7183df6938421d144784197dfbf9f931f8c396d2 Mon Sep 17 00:00:00 2001 From: giovanni Date: Wed, 4 Feb 2026 09:17:16 +0700 Subject: [PATCH 03/12] add query adjustment stock at closing sapronak --- ...260203034048_add_field_adj_number.down.sql | 6 + ...20260203034048_add_field_adj_number.up.sql | 10 ++ internal/entities/adjustment_stock.go | 1 + .../repositories/closing.repository.go | 133 ++++++++++++++++-- .../adjustment_stock.repository.go | 73 ++++++++++ .../services/adjustment.service.go | 5 + internal/utils/constant.go | 7 +- 7 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 internal/database/migrations/20260203034048_add_field_adj_number.down.sql create mode 100644 internal/database/migrations/20260203034048_add_field_adj_number.up.sql diff --git a/internal/database/migrations/20260203034048_add_field_adj_number.down.sql b/internal/database/migrations/20260203034048_add_field_adj_number.down.sql new file mode 100644 index 00000000..48bb2b54 --- /dev/null +++ b/internal/database/migrations/20260203034048_add_field_adj_number.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE adjustment_stocks +DROP COLUMN adj_number; + +COMMIT; diff --git a/internal/database/migrations/20260203034048_add_field_adj_number.up.sql b/internal/database/migrations/20260203034048_add_field_adj_number.up.sql new file mode 100644 index 00000000..1517bbea --- /dev/null +++ b/internal/database/migrations/20260203034048_add_field_adj_number.up.sql @@ -0,0 +1,10 @@ +BEGIN; + +ALTER TABLE adjustment_stocks +ADD COLUMN adj_number VARCHAR(255); + +UPDATE adjustment_stocks +SET adj_number = CONCAT('ADJ-', LPAD(id::text, 5, '0')) +WHERE adj_number IS NULL; + +COMMIT; diff --git a/internal/entities/adjustment_stock.go b/internal/entities/adjustment_stock.go index 841e4820..9ccf9246 100644 --- a/internal/entities/adjustment_stock.go +++ b/internal/entities/adjustment_stock.go @@ -11,6 +11,7 @@ type AdjustmentStock struct { PendingQty float64 `gorm:"column:pending_qty;default:0"` CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` + AdjNumber string `gorm:"column:adj_number;uniqueIndex;not null"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"` diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index cd5ce2da..9b7c5bff 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -102,12 +102,12 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak if len(params.WarehouseIDs) == 0 { return []SapronakRow{}, 0, nil } - unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL) - args = append(args, params.WarehouseIDs, params.WarehouseIDs) + unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs) case validation.SapronakTypeOutgoing: if len(params.WarehouseIDs) > 0 { - unionParts = append(unionParts, sapronakOutgoingTransfersSQL) - args = append(args, params.WarehouseIDs) + unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs) } if len(params.ProjectFlockKandangIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) @@ -174,12 +174,12 @@ func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params S if len(params.WarehouseIDs) == 0 { return []SapronakSummaryRow{}, nil } - unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL) - args = append(args, params.WarehouseIDs, params.WarehouseIDs) + unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs) case validation.SapronakTypeOutgoing: if len(params.WarehouseIDs) > 0 { - unionParts = append(unionParts, sapronakOutgoingTransfersSQL) - args = append(args, params.WarehouseIDs) + unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs) } if len(params.ProjectFlockKandangIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) @@ -456,7 +456,7 @@ SELECT COALESCE(pi.received_date, '1970-01-01') AS sort_date, COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text, COALESCE(p.po_number, '') AS reference_number, - 'Purchase' AS transaction_type, + 'Pembelian' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -505,7 +505,7 @@ SELECT st.transfer_date AS sort_date, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, st.movement_number AS reference_number, - 'Internal Transfer In' AS transaction_type, + 'Mutasi' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -549,13 +549,63 @@ JOIN uoms u ON u.id = prod.uom_id WHERE st.to_warehouse_id IN ? ` + sapronakIncomingAdjustmentsSQL = ` +SELECT + CAST(ast.id AS BIGINT) AS id, + ast.created_at AS sort_date, + COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text, + COALESCE(ast.adj_number, '') AS reference_number, + 'Adjustment stock' AS transaction_type, + prod.name AS product_name, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_category, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, + COALESCE(w.name, '') AS source_warehouse, + '-' AS destination_warehouse, + '' AS destination, + COALESCE(ast.total_qty, 0) AS quantity, + u.id AS unit_id, + u.name AS unit, + '-' AS notes +FROM adjustment_stocks ast +JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id +JOIN warehouses w ON w.id = pw.warehouse_id +JOIN products prod ON prod.id = pw.product_id +JOIN uoms u ON u.id = prod.uom_id +WHERE pw.warehouse_id IN ? + AND COALESCE(ast.total_qty, 0) <> 0 +` + sapronakOutgoingTransfersSQL = ` SELECT CAST(st.id AS BIGINT) AS id, st.transfer_date AS sort_date, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, st.movement_number AS reference_number, - 'Internal Transfer Out' AS transaction_type, + 'Mutasi' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -599,13 +649,70 @@ JOIN uoms u ON u.id = prod.uom_id WHERE st.from_warehouse_id IN ? ` + sapronakOutgoingAdjustmentsSQL = ` +SELECT + CAST(ast.id AS BIGINT) AS id, + ast.created_at AS sort_date, + COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text, + COALESCE(ast.adj_number, '') AS reference_number, + 'Adjustment stock' AS transaction_type, + prod.name AS product_name, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_category, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, + COALESCE(w.name, '') AS source_warehouse, + '-' AS destination_warehouse, + '' AS destination, + COALESCE(ast.usage_qty, 0) AS quantity, + u.id AS unit_id, + u.name AS unit, + '-' AS notes +FROM adjustment_stocks ast +JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id +JOIN warehouses w ON w.id = pw.warehouse_id +JOIN products prod ON prod.id = pw.product_id +JOIN uoms u ON u.id = prod.uom_id +WHERE pw.warehouse_id IN ? + AND COALESCE(ast.usage_qty, 0) <> 0 + AND EXISTS ( + SELECT 1 + FROM flags f + WHERE f.flagable_id = pw.product_id + AND f.flagable_type = 'products' + AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') + ) +` + sapronakOutgoingMarketingsSQL = ` SELECT CAST(mp.id AS BIGINT) AS id, m.so_date AS sort_date, TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text, m.so_number AS reference_number, - 'Trading Sales' AS transaction_type, + 'Penjualan' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -653,7 +760,7 @@ WHERE pw.project_flock_kandang_id IN ? FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = 'products' - AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET') + AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') ) ` ) diff --git a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go index f62738a3..9409fd73 100644 --- a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go +++ b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go @@ -2,9 +2,13 @@ package repositories import ( "context" + "fmt" + "strconv" + "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type AdjustmentStockRepository interface { @@ -12,6 +16,7 @@ type AdjustmentStockRepository interface { GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) WithTx(tx *gorm.DB) AdjustmentStockRepository DB() *gorm.DB + GenerateSequentialNumber(ctx context.Context, prefix string) (string, error) } type adjustmentStockRepositoryImpl struct { @@ -50,3 +55,71 @@ func (r *adjustmentStockRepositoryImpl) WithTx(tx *gorm.DB) AdjustmentStockRepos func (r *adjustmentStockRepositoryImpl) DB() *gorm.DB { return r.db } + +func (r *adjustmentStockRepositoryImpl) GenerateSequentialNumber(ctx context.Context, prefix string) (string, error) { + var values []string + err := r.db.WithContext(ctx). + Model(&entity.AdjustmentStock{}). + Where(fmt.Sprintf("%s ILIKE ?", "adj_number"), prefix+"%"). + Select("adj_number"). + Order(fmt.Sprintf("%s DESC", "adj_number")). + Limit(20). + Clauses(clause.Locking{Strength: "UPDATE"}). + Pluck("adj_number", &values).Error + if err != nil { + return "", err + } + + next := 1 + for _, value := range values { + if number, ok := parseNumericSuffix(value, prefix); ok { + next = number + 1 + break + } + } + + const maxAttempts = 20 + for attempt := 0; attempt < maxAttempts; attempt++ { + candidate := fmt.Sprintf("%s%0*d", prefix, 5, next) + exists, err := r.numberExists(ctx, r.db, candidate) + if err != nil { + return "", err + } + if !exists { + return candidate, nil + } + next++ + } + + return "", fmt.Errorf("unable to generate unique %s", "adj_number") +} + +func (r *adjustmentStockRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, value string) (bool, error) { + var count int64 + if err := db.WithContext(ctx). + Model(&entity.AdjustmentStock{}). + Where(fmt.Sprintf("%s = ?", "adj_number"), value). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func parseNumericSuffix(value, prefix string) (int, bool) { + if !strings.HasPrefix(value, prefix) { + return 0, false + } + suffix := strings.TrimPrefix(value, prefix) + if suffix == "" { + return 0, false + } + trimmed := strings.TrimLeft(suffix, "0") + if trimmed == "" { + trimmed = "0" + } + number, err := strconv.Atoi(trimmed) + if err != nil { + return 0, false + } + return number, true +} diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 862d6991..a763a6c6 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -200,6 +200,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e adjustmentStock := &entity.AdjustmentStock{ ProductWarehouseId: productWarehouse.Id, } + code, err := s.AdjustmentStockRepository.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix) + if err != nil { + return err + } + adjustmentStock.AdjNumber = code if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record") diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 9abd6a30..3b8ee054 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -329,9 +329,10 @@ const ( PurchaseStepReceiving approvalutils.ApprovalStep = 4 PurchaseStepCompleted approvalutils.ApprovalStep = 5 - PurchasePRNumberPrefix = "PR-LTI-" - PurchasePONumberPrefix = "PO-LTI-" - PurchaseNumberPadding = 4 + PurchasePRNumberPrefix = "PR-LTI-" + PurchasePONumberPrefix = "PO-LTI-" + AdjustmentStockNumberPrefix = "ADJ-" + PurchaseNumberPadding = 4 ) var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ From 90de167fcd700eacc46ca18780748a8c1c270b9f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 09:59:15 +0700 Subject: [PATCH 04/12] FEAT[BE] :add type filtering and validation to product warehouse services --- .../product_warehouse.controller.go | 1 + .../product_warehouse.repository.go | 7 ++-- .../services/product_warehouse.service.go | 23 ++++++++++- .../product_warehouse.validation.go | 1 + .../services/deliveryorder.service.go | 30 +++++++------- .../marketing/services/salesorder.service.go | 40 +++++++++++-------- .../validations/salesorder.validation.go | 2 +- internal/utils/constant.go | 2 +- 8 files changed, 70 insertions(+), 36 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index 47d85a65..bc6cdaed 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -32,6 +32,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { Flags: c.Query("flags", ""), KandangId: uint(c.QueryInt("kandang_id", 0)), TransferContext: c.Query(utils.TransferContextKey, ""), + Type: c.Query("type", ""), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index a7fe452b..e49fc421 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -168,9 +168,10 @@ func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []s } return db. - Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products"). - Where("flags.name IN ?", flags) + Joins("JOIN products p_flag ON p_flag.id = product_warehouses.product_id"). + Joins("JOIN flags f_flag ON f_flag.flagable_id = p_flag.id AND f_flag.flagable_type = ?", "products"). + Where("f_flag.name IN ?", flags). + Distinct() } func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error { diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 5bb3f692..98656de1 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -99,6 +99,12 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) offset := (params.Page - 1) * params.Limit + if params.Type != "" { + if !utils.IsValidMarketingType(params.Type) { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing type") + } + } + cleanFlags := utils.ParseFlags(params.Flags) productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { @@ -128,7 +134,22 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) db = db.Where("warehouse_id = ?", params.WarehouseId) } - db = s.Repository.ApplyFlagsFilter(db, cleanFlags) + if params.Type != "" { + switch params.Type { + case string(utils.MarketingTypeAyamPullet): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}) + case string(utils.MarketingTypeAyam): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati)}) + case string(utils.MarketingTypeTelur): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak)}) + case string(utils.MarketingTypeTrading): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher), string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia), string(utils.FlagEkspedisi)}) + } + } + + if len(cleanFlags) > 0 { + db = s.Repository.ApplyFlagsFilter(db, cleanFlags) + } return db.Order("product_warehouses.id DESC") }) diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 61a41ad0..7e7da7a6 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -20,4 +20,5 @@ type Query struct { Flags string `query:"flags" validate:"omitempty"` KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"` + Type string `query:"type" validate:"omitempty,oneof=AYAM TELUR TRADING AYAM_PULLET"` } diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 80045027..268a81eb 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -289,13 +289,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery itemDeliveryDate = &parsedDate } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - var totalPrice float64 - if marketing.MarketingType == string(utils.MarketingTypeTrading) { - totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice - } else { - totalPrice = totalWeight * requestedProduct.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, requestedProduct.Qty, requestedProduct.AvgWeight, requestedProduct.UnitPrice, foundMarketingProduct.Week) deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice @@ -421,13 +415,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO itemDeliveryDate = deliveryProduct.DeliveryDate } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - var totalPrice float64 - if marketing.MarketingType == string(utils.MarketingTypeTrading) { - totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice - } else { - totalPrice = totalWeight * requestedProduct.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, requestedProduct.Qty, requestedProduct.AvgWeight, requestedProduct.UnitPrice, foundMarketingProduct.Week) deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice @@ -471,6 +459,20 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO return s.getMarketingWithDeliveries(c, id) } +func (s *deliveryOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) { + if marketingType == string(utils.MarketingTypeTrading) { + totalWeight = 0 + totalPrice = qty * unitPrice + } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { + totalWeight = qty * avgWeight + totalPrice = unitPrice * float64(*week) * qty + } else { + totalWeight = qty * avgWeight + totalPrice = totalWeight * unitPrice + } + return totalWeight, totalPrice +} + func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64, actorID uint) error { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index a43370d5..a64caa9f 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -104,7 +104,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } if !utils.IsValidMarketingType(req.MarketingType) { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM PULLET") + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") } actorID, err := m.ActorIDFromContext(c) @@ -119,6 +119,9 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } for _, item := range req.MarketingProducts { + if req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type") + } if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") } @@ -215,7 +218,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } if req.MarketingType != "" && !utils.IsValidMarketingType(req.MarketingType) { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM PULLET") + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") } if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { @@ -245,6 +248,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if len(req.MarketingProducts) > 0 { for _, item := range req.MarketingProducts { + if req.MarketingType != "" && req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type") + } if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") } @@ -331,13 +337,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u for _, rp := range req.MarketingProducts { if old, ok := oldByPW[rp.ProductWarehouseId]; ok { - totalWeight := rp.Qty * rp.AvgWeight - var totalPrice float64 - if marketing.MarketingType == string(utils.MarketingTypeTrading) { - totalPrice = rp.Qty * rp.UnitPrice - } else { - totalPrice = totalWeight * rp.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week) deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -688,13 +688,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, marketingType string, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { - totalWeight := rp.Qty * rp.AvgWeight - var totalPrice float64 - if marketingType == string(utils.MarketingTypeTrading) { - totalPrice = rp.Qty * rp.UnitPrice - } else { - totalPrice = totalWeight * rp.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week) marketingProduct := &entity.MarketingProduct{ MarketingId: marketingId, @@ -730,3 +724,17 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont return nil } + +func (s *salesOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) { + if marketingType == string(utils.MarketingTypeTrading) { + totalWeight = 0 + totalPrice = qty * unitPrice + } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { + totalWeight = qty * avgWeight + totalPrice = unitPrice * float64(*week) * qty + } else { + totalWeight = qty * avgWeight + totalPrice = totalWeight * unitPrice + } + return totalWeight, totalPrice +} diff --git a/internal/modules/marketing/validations/salesorder.validation.go b/internal/modules/marketing/validations/salesorder.validation.go index 9a3cee29..bf38417f 100644 --- a/internal/modules/marketing/validations/salesorder.validation.go +++ b/internal/modules/marketing/validations/salesorder.validation.go @@ -17,7 +17,7 @@ type CreateMarketingProduct struct { ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` Qty float64 `json:"qty" validate:"required,gt=0"` - AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"omitempty,gt=0"` } type Update struct { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index cb8a0ba2..1de04fa3 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -222,7 +222,7 @@ const ( MarketingTypeAyam MarketingType = "AYAM" MarketingTypeTelur MarketingType = "TELUR" MarketingTypeTrading MarketingType = "TRADING" - MarketingTypeAyamPullet MarketingType = "AYAM PULLET" + MarketingTypeAyamPullet MarketingType = "AYAM_PULLET" ) // ------------------------------------------------------------------- From 474c42770b507e514631bd32e606701d34783be5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 11:46:32 +0700 Subject: [PATCH 05/12] FEAT[BE] :add week calculation and chickin preload to product warehouse services --- .../dto/product_warehouse.dto.go | 73 +++++++++++++++++++ .../services/product_warehouse.service.go | 3 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index b8f51c52..b9c95004 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -6,6 +6,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) // === DTO Structs === @@ -22,6 +23,7 @@ type ProductWarehouseListDTO struct { Product *productDTO.ProductRelationDTO `json:"product,omitempty"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"` + Week int `json:"week"` CreatedUser *UserRelationDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -109,6 +111,22 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT } dto.ProjectFlockKandang = pfkDTO + + // Calculate week for AYAM_PULLET/AYAM products + productFlags := make([]string, len(e.Product.Flags)) + for i, f := range e.Product.Flags { + productFlags[i] = f.Name + } + + var category string + if e.ProjectFlockKandang.ProjectFlock.Id != 0 { + category = e.ProjectFlockKandang.ProjectFlock.Category + } + + now := time.Now() + _, ageInWeeks := calculateAgeFromChickin(e.ProjectFlockKandang, &now, productFlags, category) + + dto.Week = ageInWeeks } return dto @@ -138,3 +156,58 @@ func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNeste Warehouse: &warehouse, } } + +// Helper function to calculate age from chickin (same logic as closingMarketing.dto.go) +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, currentDate *time.Time, productFlags []string, category string) (int, int) { + if projectFlockKandang == nil || currentDate == nil || len(projectFlockKandang.Chickins) == 0 { + return 0, 0 + } + + // Return 0 for TRADING, TELUR, and AYAM flags (only AYAM_PULLET should have week) + for _, flag := range productFlags { + if flag == string(utils.FlagOVK) || + flag == string(utils.FlagPakan) || + flag == string(utils.FlagPreStarter) || + flag == string(utils.FlagStarter) || + flag == string(utils.FlagFinisher) || + flag == string(utils.FlagObat) || + flag == string(utils.FlagVitamin) || + flag == string(utils.FlagKimia) || + flag == string(utils.FlagEkspedisi) || + flag == string(utils.FlagTelur) || + flag == string(utils.FlagTelurUtuh) || + flag == string(utils.FlagTelurPecah) || + flag == string(utils.FlagTelurPutih) || + flag == string(utils.FlagTelurRetak) || + flag == string(utils.FlagAyamAfkir) || + flag == string(utils.FlagAyamCulling) || + flag == string(utils.FlagAyamMati) { + return 0, 0 + } + } + + // Find earliest chickin date + earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ChickInDate.Before(earliestChickinDate) { + earliestChickinDate = chickin.ChickInDate + } + } + + diff := currentDate.Sub(earliestChickinDate) + ageInDays := int(diff.Hours() / 24) + + var ageInWeeks int + if ageInDays <= 0 { + ageInWeeks = 0 + } else { + if category == string(utils.ProjectFlockCategoryLaying) { + ageInDays = ageInDays + 119 + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } else { + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } + } + + return ageInDays, ageInWeeks +} diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 98656de1..7132644e 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -46,7 +46,8 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { Preload("Warehouse.Area"). Preload("Warehouse.Kandang"). Preload("ProjectFlockKandang"). - Preload("ProjectFlockKandang.ProjectFlock") + Preload("ProjectFlockKandang.ProjectFlock"). + Preload("ProjectFlockKandang.Chickins") } func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { From 357b5709f50c9142c8809eab1e79e337e08eda97 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 12:48:05 +0700 Subject: [PATCH 06/12] FEAT[BE] :add conversion fields and week tracking to marketing product DTOs and update mapping functions --- .../marketing/dto/deliveryorder.dto.go | 61 ++++++++++++------- .../modules/marketing/dto/salesorder.dto.go | 49 +++++++++------ .../services/deliveryorder.service.go | 2 + .../marketing/services/salesorder.service.go | 1 + 4 files changed, 73 insertions(+), 40 deletions(-) diff --git a/internal/modules/marketing/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index 4bcbacca..451856c2 100644 --- a/internal/modules/marketing/dto/deliveryorder.dto.go +++ b/internal/modules/marketing/dto/deliveryorder.dto.go @@ -76,16 +76,20 @@ type DeliveryGroupDTO struct { } type DeliveryMarketingProductDTO struct { - Id uint `json:"id"` - MarketingId uint `json:"marketing_id"` - ProductWarehouseId uint `json:"product_warehouse_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` - VehicleNumber string `json:"vehicle_number,omitempty"` + Id uint `json:"id"` + MarketingId uint `json:"marketing_id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ConvertionUnit *string `json:"convertion_unit,omitempty"` + WeightPerConvertion *float64 `json:"weight_per_convertion,omitempty"` + TotalPeti *float64 `json:"total_peti,omitempty"` + Week *int `json:"week,omitempty"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` + VehicleNumber string `json:"vehicle_number,omitempty"` } func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO { @@ -97,24 +101,35 @@ func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO { } } -func ToDeliveryMarketingProductDTO(e entity.MarketingProduct) DeliveryMarketingProductDTO { +func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType string) DeliveryMarketingProductDTO { var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO if e.ProductWarehouse.Id != 0 { mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) productWarehouse = &mapped } + // Calculate total_peti only for TELUR marketing type + var totalPeti *float64 + if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 { + calculated := e.TotalWeight / *e.WeightPerConvertion + totalPeti = &calculated + } + return DeliveryMarketingProductDTO{ - Id: e.Id, - MarketingId: e.MarketingId, - ProductWarehouseId: e.ProductWarehouseId, - Qty: e.Qty, - UnitPrice: e.UnitPrice, - AvgWeight: e.AvgWeight, - TotalWeight: e.TotalWeight, - TotalPrice: e.TotalPrice, - ProductWarehouse: productWarehouse, - VehicleNumber: getVehicleNumber(e), + Id: e.Id, + MarketingId: e.MarketingId, + ProductWarehouseId: e.ProductWarehouseId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + ConvertionUnit: e.ConvertionUnit, + WeightPerConvertion: e.WeightPerConvertion, + TotalPeti: totalPeti, + Week: e.Week, + ProductWarehouse: productWarehouse, + VehicleNumber: getVehicleNumber(e), } } @@ -161,7 +176,7 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M if len(marketing.Products) > 0 { salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products)) for i, product := range marketing.Products { - salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product) + salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType) } } @@ -201,7 +216,7 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity if len(marketing.Products) > 0 { salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products)) for i, product := range marketing.Products { - salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product) + salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType) } } diff --git a/internal/modules/marketing/dto/salesorder.dto.go b/internal/modules/marketing/dto/salesorder.dto.go index 86bd5f84..866fe268 100644 --- a/internal/modules/marketing/dto/salesorder.dto.go +++ b/internal/modules/marketing/dto/salesorder.dto.go @@ -10,13 +10,17 @@ import ( // === DTO Structs === type MarketingProductDTO struct { - Id uint `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ConvertionUnit *string `json:"convertion_unit,omitempty"` + WeightPerConvertion *float64 `json:"weight_per_convertion,omitempty"` + TotalPeti *float64 `json:"total_peti,omitempty"` + Week *int `json:"week,omitempty"` + ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` } type SalesOrdersListDTO struct { @@ -29,7 +33,7 @@ type SalesOrdersListDTO struct { // === Mapper Functions === -func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { +func ToMarketingProductDTO(e entity.MarketingProduct, marketingType string) MarketingProductDTO { var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO if e.ProductWarehouse.Id != 0 { @@ -37,21 +41,32 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { productWarehouse = &mapped } + // Calculate total_peti only for TELUR marketing type + var totalPeti *float64 + if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 { + calculated := e.TotalWeight / *e.WeightPerConvertion + totalPeti = &calculated + } + return MarketingProductDTO{ - Id: e.Id, - Qty: e.Qty, - UnitPrice: e.UnitPrice, - AvgWeight: e.AvgWeight, - TotalWeight: e.TotalWeight, - TotalPrice: e.TotalPrice, - ProductWarehouse: productWarehouse, + Id: e.Id, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + ConvertionUnit: e.ConvertionUnit, + WeightPerConvertion: e.WeightPerConvertion, + TotalPeti: totalPeti, + Week: e.Week, + ProductWarehouse: productWarehouse, } } func ToSalesOrdersListDTO(e entity.Marketing) SalesOrdersListDTO { products := make([]MarketingProductDTO, len(e.Products)) for i, p := range e.Products { - products[i] = ToMarketingProductDTO(p) + products[i] = ToMarketingProductDTO(p, e.MarketingType) } return SalesOrdersListDTO{ @@ -68,7 +83,7 @@ func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { if len(e.Products) > 0 { salesOrder = make([]MarketingProductDTO, len(e.Products)) for i, product := range e.Products { - salesOrder[i] = ToMarketingProductDTO(product) + salesOrder[i] = ToMarketingProductDTO(product, e.MarketingType) } } diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 268a81eb..493f689f 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -65,6 +65,7 @@ func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") } @@ -111,6 +112,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index a64caa9f..ffc53d79 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -69,6 +69,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product.Flags"). + Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse") } From 14cc7ef2ae8b593cf36a6cd422821ee32233cfeb Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 4 Feb 2026 13:31:20 +0700 Subject: [PATCH 07/12] [FEAT/BE] Add field purchase response get all --- .../common/service/common.fifo.service.go | 109 ++++++++++++------ .../modules/purchases/dto/purchase.dto.go | 64 +++++++++- 2 files changed, 131 insertions(+), 42 deletions(-) diff --git a/internal/common/service/common.fifo.service.go b/internal/common/service/common.fifo.service.go index 190bf819..fd1812fb 100644 --- a/internal/common/service/common.fifo.service.go +++ b/internal/common/service/common.fifo.service.go @@ -743,8 +743,6 @@ func (s *fifoService) releaseUsagePortion( if expectedWarehouseID == 0 || alloc.ProductWarehouseId == expectedWarehouseID { continue } - fmt.Printf("WARN[FIFO] ALLOC WAREHOUSE MISMATCH usable_key=%s usable_id=%d alloc_id=%d expected_pw=%d actual_pw=%d\n", - usableKey.String(), usableID, alloc.Id, expectedWarehouseID, alloc.ProductWarehouseId) if err := tx.Model(&entities.StockAllocation{}). Where("id = ?", alloc.Id). Update("product_warehouse_id", expectedWarehouseID).Error; err != nil { @@ -848,41 +846,80 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p cfg.Columns.CreatedAt, ) - var rows []struct { - ID uint - Pending float64 - CreatedAt time.Time - } - - query := tx.Table(cfg.Table). - Select(selectStmt). - Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID). - Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)). - Limit(s.pendingBatchPerUsable) - - if cfg.Scope != nil { - query = cfg.Scope(query) - } - - for _, order := range s.orderClauses(cfg.OrderBy) { - query = query.Order(order) - } - - if err := query.Find(&rows).Error; err != nil { - return nil, err - } - - for _, row := range rows { - if row.Pending <= 0 { - continue + if cfg.Columns.CreatedAt == cfg.Columns.ID { + var rows []struct { + ID uint + Pending float64 + CreatedAt int64 + } + + query := tx.Table(cfg.Table). + Select(selectStmt). + Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID). + Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)). + Limit(s.pendingBatchPerUsable) + + if cfg.Scope != nil { + query = cfg.Scope(query) + } + + for _, order := range s.orderClauses(cfg.OrderBy) { + query = query.Order(order) + } + + if err := query.Find(&rows).Error; err != nil { + return nil, err + } + + for _, row := range rows { + if row.Pending <= 0 { + continue + } + candidates = append(candidates, pendingCandidate{ + UsableKey: key, + Config: cfg, + UsableID: row.ID, + Pending: row.Pending, + CreatedAt: time.Unix(0, row.CreatedAt), + }) + } + } else { + var rows []struct { + ID uint + Pending float64 + CreatedAt time.Time + } + + query := tx.Table(cfg.Table). + Select(selectStmt). + Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID). + Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)). + Limit(s.pendingBatchPerUsable) + + if cfg.Scope != nil { + query = cfg.Scope(query) + } + + for _, order := range s.orderClauses(cfg.OrderBy) { + query = query.Order(order) + } + + if err := query.Find(&rows).Error; err != nil { + return nil, err + } + + for _, row := range rows { + if row.Pending <= 0 { + continue + } + candidates = append(candidates, pendingCandidate{ + UsableKey: key, + Config: cfg, + UsableID: row.ID, + Pending: row.Pending, + CreatedAt: row.CreatedAt, + }) } - candidates = append(candidates, pendingCandidate{ - UsableKey: key, - Config: cfg, - UsableID: row.ID, - Pending: row.Pending, - CreatedAt: row.CreatedAt, - }) } } diff --git a/internal/modules/purchases/dto/purchase.dto.go b/internal/modules/purchases/dto/purchase.dto.go index 1956729c..444c41f0 100644 --- a/internal/modules/purchases/dto/purchase.dto.go +++ b/internal/modules/purchases/dto/purchase.dto.go @@ -1,6 +1,7 @@ package dto import ( + "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -24,12 +25,17 @@ type PurchaseRelationDTO struct { type PurchaseListDTO struct { PurchaseRelationDTO - Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` - DueDate *time.Time `json:"due_date"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"` + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + DueDate *time.Time `json:"due_date"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + RequesterName string `json:"requester_name"` + PoExpedition []string `json:"po_expedition"` + Products []productDTO.ProductRelationDTO `json:"products"` + Location *locationDTO.LocationRelationDTO `json:"location"` + Area *areaDTO.AreaRelationDTO `json:"area"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"` } type PurchaseDetailDTO struct { @@ -146,6 +152,10 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO { mapped := userDTO.ToUserRelationDTO(p.CreatedUser) createdUser = &mapped } + requesterName := "" + if createdUser != nil { + requesterName = createdUser.Name + } var latestApproval *approvalDTO.ApprovalRelationDTO if p.LatestApproval != nil && p.LatestApproval.Id != 0 { @@ -153,11 +163,53 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO { latestApproval = &mapped } + var ( + poExpedition []string + location *locationDTO.LocationRelationDTO + area *areaDTO.AreaRelationDTO + ) + productMap := make(map[uint]productDTO.ProductRelationDTO) + expeditionRefSet := make(map[string]struct{}) + for i := range p.Items { + item := p.Items[i] + if item.Product != nil && item.Product.Id != 0 { + if _, exists := productMap[item.Product.Id]; !exists { + productMap[item.Product.Id] = productDTO.ToProductRelationDTO(*item.Product) + } + } + if item.ExpenseNonstock != nil && item.ExpenseNonstock.Expense != nil { + ref := strings.TrimSpace(item.ExpenseNonstock.Expense.ReferenceNumber) + if ref != "" { + if _, exists := expeditionRefSet[ref]; !exists { + expeditionRefSet[ref] = struct{}{} + poExpedition = append(poExpedition, ref) + } + } + } + if location == nil && item.Warehouse != nil && item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 { + loc := locationDTO.ToLocationRelationDTO(*item.Warehouse.Location) + location = &loc + } + if area == nil && item.Warehouse != nil && item.Warehouse.Area.Id != 0 { + ar := areaDTO.ToAreaRelationDTO(item.Warehouse.Area) + area = &ar + } + } + products := make([]productDTO.ProductRelationDTO, 0, len(productMap)) + for _, prod := range productMap { + products = append(products, prod) + } + return PurchaseListDTO{ PurchaseRelationDTO: ToPurchaseRelationDTO(&p), Supplier: supplier, DueDate: p.DueDate, CreatedUser: createdUser, + RequesterName: requesterName, + PoExpedition: poExpedition, + Products: products, + Location: location, + Area: area, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, LatestApproval: latestApproval, From 114f1a7c24ed7bf396ae01f0f461d923643d6450 Mon Sep 17 00:00:00 2001 From: giovanni Date: Wed, 4 Feb 2026 13:51:55 +0700 Subject: [PATCH 08/12] fix hasil produksi deplesi std dan filter recording approved --- .../production_result.repository.go | 21 ++++++++++++++++++- .../repports/services/repport.service.go | 3 +++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/modules/repports/repositories/production_result.repository.go b/internal/modules/repports/repositories/production_result.repository.go index a8eccb91..a708be9e 100644 --- a/internal/modules/repports/repositories/production_result.repository.go +++ b/internal/modules/repports/repositories/production_result.repository.go @@ -31,9 +31,25 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang( return []entity.Recording{}, 0, nil } + latestApproval := r.db.WithContext(ctx). + Table("approvals AS a"). + Select("a.approvable_id, a.action, a.step_number"). + 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.ApprovalWorkflowRecording), + ) + countQuery := r.db.WithContext(ctx). Model(&entity.Recording{}). - Where("project_flock_kandangs_id = ?", projectFlockKandangID) + Joins("JOIN (?) AS la ON la.approvable_id = recordings.id", latestApproval). + Where("project_flock_kandangs_id = ?", projectFlockKandangID). + Where("la.step_number = ?", utils.RecordingStepDisetujui). + Where("la.action = ?", string(entity.ApprovalActionApproved)) var total int64 if err := countQuery.Count(&total).Error; err != nil { @@ -59,7 +75,10 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang( dataQuery := r.db.WithContext(ctx). Model(&entity.Recording{}). + Joins("JOIN (?) AS la ON la.approvable_id = recordings.id", latestApproval). Where("project_flock_kandangs_id = ?", projectFlockKandangID). + Where("la.step_number = ?", utils.RecordingStepDisetujui). + Where("la.action = ?", string(entity.ApprovalActionApproved)). Preload("Eggs", func(db *gorm.DB) *gorm.DB { return db.Select("recording_eggs.*, f.name AS product_flag_name"). Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id"). diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index d45cba62..db9fe3f1 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -398,6 +398,9 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation. if detail != nil && detail.TargetMeanBw != nil { weeklyResults[i].StdBw = *detail.TargetMeanBw } + if detail != nil { + weeklyResults[i].DepStd = valueOrZero(detail.MaxDepletion) + } } } From 1d9597636013ef7dad7b2355fed641df9001fcea Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 14:47:56 +0700 Subject: [PATCH 09/12] FEAT[BE] :add marketing type field to delivery and sales order DTOs, enhance validation and service logic for consistent marketing type handling --- .../marketing/dto/deliveryorder.dto.go | 5 +- .../modules/marketing/dto/salesorder.dto.go | 5 +- .../marketing/services/salesorder.service.go | 65 +++++++++++++------ .../validations/salesorder.validation.go | 3 +- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/internal/modules/marketing/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index 451856c2..bd4b2a0b 100644 --- a/internal/modules/marketing/dto/deliveryorder.dto.go +++ b/internal/modules/marketing/dto/deliveryorder.dto.go @@ -2,6 +2,7 @@ package dto import ( "fmt" + "math" "sort" "time" @@ -79,6 +80,7 @@ type DeliveryMarketingProductDTO struct { Id uint `json:"id"` MarketingId uint `json:"marketing_id"` ProductWarehouseId uint `json:"product_warehouse_id"` + MarketingType string `json:"marketing_type"` Qty float64 `json:"qty"` UnitPrice float64 `json:"unit_price"` AvgWeight float64 `json:"avg_weight"` @@ -111,7 +113,7 @@ func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType stri // Calculate total_peti only for TELUR marketing type var totalPeti *float64 if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 { - calculated := e.TotalWeight / *e.WeightPerConvertion + calculated := math.Floor(e.TotalWeight / *e.WeightPerConvertion) totalPeti = &calculated } @@ -119,6 +121,7 @@ func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType stri Id: e.Id, MarketingId: e.MarketingId, ProductWarehouseId: e.ProductWarehouseId, + MarketingType: marketingType, Qty: e.Qty, UnitPrice: e.UnitPrice, AvgWeight: e.AvgWeight, diff --git a/internal/modules/marketing/dto/salesorder.dto.go b/internal/modules/marketing/dto/salesorder.dto.go index 866fe268..11479036 100644 --- a/internal/modules/marketing/dto/salesorder.dto.go +++ b/internal/modules/marketing/dto/salesorder.dto.go @@ -1,6 +1,7 @@ package dto import ( + "math" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -11,6 +12,7 @@ import ( type MarketingProductDTO struct { Id uint `json:"id"` + MarketingType string `json:"marketing_type"` Qty float64 `json:"qty"` UnitPrice float64 `json:"unit_price"` AvgWeight float64 `json:"avg_weight"` @@ -44,12 +46,13 @@ func ToMarketingProductDTO(e entity.MarketingProduct, marketingType string) Mark // Calculate total_peti only for TELUR marketing type var totalPeti *float64 if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 { - calculated := e.TotalWeight / *e.WeightPerConvertion + calculated := math.Floor(e.TotalWeight / *e.WeightPerConvertion) totalPeti = &calculated } return MarketingProductDTO{ Id: e.Id, + MarketingType: marketingType, Qty: e.Qty, UnitPrice: e.UnitPrice, AvgWeight: e.AvgWeight, diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index ffc53d79..58901794 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -104,8 +104,23 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, err } - if !utils.IsValidMarketingType(req.MarketingType) { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") + // Validasi semua product harus punya marketing_type yang sama + if len(req.MarketingProducts) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "marketing_products is required") + } + + firstMarketingType := req.MarketingProducts[0].MarketingType + if !utils.IsValidMarketingType(firstMarketingType) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid") + } + + for i, item := range req.MarketingProducts { + if !utils.IsValidMarketingType(item.MarketingType) { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1)) + } + if item.MarketingType != firstMarketingType { + return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama") + } } actorID, err := m.ActorIDFromContext(c) @@ -120,11 +135,11 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } for _, item := range req.MarketingProducts { - if req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type") + if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi") } if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") + return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid") } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err @@ -160,7 +175,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e SoDate: soDate, SalesPersonId: req.SalesPersonId, Notes: req.Notes, - MarketingType: req.MarketingType, + MarketingType: firstMarketingType, CreatedBy: actorID, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { @@ -173,7 +188,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e if product.ProductWarehouseId != 0 { pwIDs = append(pwIDs, product.ProductWarehouseId) } - if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, marketing.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } @@ -218,8 +233,21 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, err } - if req.MarketingType != "" && !utils.IsValidMarketingType(req.MarketingType) { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") + // Validasi semua product harus punya marketing_type yang sama + if len(req.MarketingProducts) > 0 { + firstMarketingType := req.MarketingProducts[0].MarketingType + if !utils.IsValidMarketingType(firstMarketingType) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid") + } + + for i, item := range req.MarketingProducts { + if !utils.IsValidMarketingType(item.MarketingType) { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1)) + } + if item.MarketingType != firstMarketingType { + return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama") + } + } } if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { @@ -249,11 +277,11 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if len(req.MarketingProducts) > 0 { for _, item := range req.MarketingProducts { - if req.MarketingType != "" && req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type") + if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi") } if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") + return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid") } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err @@ -302,8 +330,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if req.Notes != "" { updateBody["notes"] = req.Notes } - if req.MarketingType != "" { - updateBody["marketing_type"] = req.MarketingType + if len(req.MarketingProducts) > 0 { + updateBody["marketing_type"] = req.MarketingProducts[0].MarketingType } if len(updateBody) > 0 { @@ -330,15 +358,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u reqByPW[rp.ProductWarehouseId] = rp } - marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") - } - for _, rp := range req.MarketingProducts { if old, ok := oldByPW[rp.ProductWarehouseId]; ok { - totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week) + totalWeight, totalPrice := s.calculatePriceByMarketingType(rp.MarketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week) deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -397,7 +420,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } } else { - if err := s.createMarketingProductWithDelivery(c.Context(), id, marketing.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + if err := s.createMarketingProductWithDelivery(c.Context(), id, rp.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } diff --git a/internal/modules/marketing/validations/salesorder.validation.go b/internal/modules/marketing/validations/salesorder.validation.go index bf38417f..6d6b80b6 100644 --- a/internal/modules/marketing/validations/salesorder.validation.go +++ b/internal/modules/marketing/validations/salesorder.validation.go @@ -5,11 +5,11 @@ type Create struct { SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"` Date string `json:"date" validate:"required,datetime=2006-01-02"` Notes string `json:"notes" validate:"omitempty,max=500"` - MarketingType string `json:"marketing_type" validate:"required,min=1,max=50"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"` } type CreateMarketingProduct struct { + MarketingType string `json:"marketing_type" validate:"required,min=1,max=50"` VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` ConvertionUnit *string `json:"convertion_unit" validate:"omitempty,min=1,max=20"` WeightPerConvertion *float64 `json:"weight_per_convertion" validate:"omitempty,gt=0"` @@ -25,7 +25,6 @@ type Update struct { SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"` Date string `json:"date" validate:"omitempty,datetime=2006-01-02"` Notes string `json:"notes" validate:"omitempty,max=500"` - MarketingType string `json:"marketing_type" validate:"omitempty,min=1,max=50"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } From 1f10e962885079103da0dabd8d172dad3b834e16 Mon Sep 17 00:00:00 2001 From: giovanni Date: Wed, 4 Feb 2026 15:17:05 +0700 Subject: [PATCH 10/12] fix query field source_warehouse --- internal/modules/closings/repositories/closing.repository.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 4f699086..a4db4694 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -566,8 +566,8 @@ SELECT FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_sub_category, - COALESCE(w.name, '') AS source_warehouse, - '-' AS destination_warehouse, + '-' AS source_warehouse, + COALESCE(w.name, '') AS destination_warehouse, '' AS destination, COALESCE(ast.total_qty, 0) AS quantity, u.id AS unit_id, From aa1fd1c35b6485f6a1bf944d9384236e6cab3e73 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 5 Feb 2026 09:57:38 +0700 Subject: [PATCH 11/12] FEAT[BE] :update price calculation in sales order service for accurate rounding, add new conversion unit for quantity --- .../modules/marketing/services/salesorder.service.go | 11 ++++++----- internal/utils/constant.go | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index 58901794..eb2e4f5b 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "strings" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" @@ -752,13 +753,13 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont func (s *salesOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) { if marketingType == string(utils.MarketingTypeTrading) { totalWeight = 0 - totalPrice = qty * unitPrice + totalPrice = math.Round(qty*unitPrice*100) / 100 } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { - totalWeight = qty * avgWeight - totalPrice = unitPrice * float64(*week) * qty + totalWeight = math.Round(qty*avgWeight*100) / 100 + totalPrice = math.Round(unitPrice*float64(*week)*qty*100) / 100 } else { - totalWeight = qty * avgWeight - totalPrice = totalWeight * unitPrice + totalWeight = math.Round(qty*avgWeight*100) / 100 + totalPrice = math.Round(totalWeight*unitPrice*100) / 100 } return totalWeight, totalPrice } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 1de04fa3..27d1ec3e 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -234,6 +234,7 @@ type ConvertionUnit string const ( ConvertionUnitPeti ConvertionUnit = "PETI" ConvertionUnitKG ConvertionUnit = "KG" + ConvertionUnitQty ConvertionUnit = "QTY" ) // ------------------------------------------------------------------- @@ -643,7 +644,7 @@ func IsValidMarketingType(v string) bool { func IsValidConvertionUnit(v string) bool { switch ConvertionUnit(v) { - case ConvertionUnitPeti, ConvertionUnitKG: + case ConvertionUnitPeti, ConvertionUnitKG, ConvertionUnitQty: return true } return false From d41f1b9495165702fc5db454ed09b143a2e9113b Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Thu, 5 Feb 2026 10:26:44 +0700 Subject: [PATCH 12/12] fix(BE): multiple filter, all search --- .../controllers/transaction.controller.go | 83 +++++++++++++------ .../services/transaction.service.go | 53 ++++++++---- .../validations/transaction.validation.go | 24 +++--- 3 files changed, 110 insertions(+), 50 deletions(-) diff --git a/internal/modules/finance/transactions/controllers/transaction.controller.go b/internal/modules/finance/transactions/controllers/transaction.controller.go index 5c25cbcd..228feeaa 100644 --- a/internal/modules/finance/transactions/controllers/transaction.controller.go +++ b/internal/modules/finance/transactions/controllers/transaction.controller.go @@ -24,46 +24,81 @@ func NewTransactionController(transactionService service.TransactionService) *Tr } func (u *TransactionController) GetAll(c *fiber.Ctx) error { - parseOptionalUint := func(key string) (*uint, error) { + parseUintListParam := func(key string) ([]uint, error) { raw := strings.TrimSpace(c.Query(key, "")) if raw == "" { return nil, nil } - parsed, err := strconv.ParseUint(raw, 10, 64) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid "+key) + parts := strings.Split(raw, ",") + ids := make([]uint, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed == "" { + return nil, strconv.ErrSyntax + } + parsed, err := strconv.ParseUint(trimmed, 10, 64) + if err != nil { + return nil, err + } + if parsed == 0 { + continue + } + ids = append(ids, uint(parsed)) } - if parsed == 0 { + if len(ids) == 0 { return nil, nil } - value := uint(parsed) - return &value, nil + return ids, nil } - bankId, err := parseOptionalUint("bank_id") - if err != nil { - return err + parseStringListParam := func(key string) ([]string, error) { + raw := strings.TrimSpace(c.Query(key, "")) + if raw == "" { + return nil, nil + } + parts := strings.Split(raw, ",") + values := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed == "" { + return nil, strconv.ErrSyntax + } + values = append(values, trimmed) + } + if len(values) == 0 { + return nil, nil + } + return values, nil } - customerId, err := parseOptionalUint("customer_id") + + bankIDs, err := parseUintListParam("bank_ids") if err != nil { - return err + return fiber.NewError(fiber.StatusBadRequest, "Invalid bank_ids") } - supplierId, err := parseOptionalUint("supplier_id") + customerIDs, err := parseUintListParam("customer_ids") if err != nil { - return err + return fiber.NewError(fiber.StatusBadRequest, "Invalid customer_ids") + } + supplierIDs, err := parseUintListParam("supplier_ids") + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_ids") + } + transactionTypes, err := parseStringListParam("transaction_types") + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_types") } query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), - TransactionType: c.Query("transaction_type", ""), - BankId: bankId, - CustomerId: customerId, - SupplierId: supplierId, - SortDate: c.Query("sort_date", ""), - StartDate: c.Query("start_date", ""), - EndDate: c.Query("end_date", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + TransactionTypes: transactionTypes, + BankIDs: bankIDs, + CustomerIDs: customerIDs, + SupplierIDs: supplierIDs, + SortDate: c.Query("sort_date", ""), + StartDate: c.Query("start_date", ""), + EndDate: c.Query("end_date", ""), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/finance/transactions/services/transaction.service.go b/internal/modules/finance/transactions/services/transaction.service.go index f422320f..4526b817 100644 --- a/internal/modules/finance/transactions/services/transaction.service.go +++ b/internal/modules/finance/transactions/services/transaction.service.go @@ -74,33 +74,58 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en if params.Search != "" { like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%" + db = db.Joins( + "LEFT JOIN customers ON customers.id = payments.party_id AND payments.party_type = ? AND customers.deleted_at IS NULL", + string(utils.PaymentPartyCustomer), + ).Joins( + "LEFT JOIN suppliers ON suppliers.id = payments.party_id AND payments.party_type = ? AND suppliers.deleted_at IS NULL", + string(utils.PaymentPartySupplier), + ).Joins( + "LEFT JOIN banks ON banks.id = payments.bank_id AND banks.deleted_at IS NULL", + ) db = db.Where( `LOWER(payment_code) LIKE ? OR LOWER(COALESCE(reference_number, '')) LIKE ? OR LOWER(COALESCE(transaction_type, '')) LIKE ? OR - LOWER(COALESCE(notes, '')) LIKE ?`, - like, like, like, like, + LOWER(COALESCE(notes, '')) LIKE ? OR + LOWER(COALESCE(customers.name, '')) LIKE ? OR + LOWER(COALESCE(suppliers.name, '')) LIKE ? OR + LOWER(COALESCE(banks.name, '')) LIKE ?`, + like, like, like, like, like, like, like, ) } - if strings.TrimSpace(params.TransactionType) != "" { - db = db.Where("transaction_type = ?", strings.ToUpper(strings.TrimSpace(params.TransactionType))) + if len(params.TransactionTypes) > 0 { + types := make([]string, 0, len(params.TransactionTypes)) + for _, transactionType := range params.TransactionTypes { + normalized := strings.ToUpper(strings.TrimSpace(transactionType)) + if normalized == "" { + continue + } + types = append(types, normalized) + } + if len(types) > 0 { + db = db.Where("transaction_type IN ?", types) + } } - if params.BankId != nil { - db = db.Where("bank_id = ?", *params.BankId) + if len(params.BankIDs) > 0 { + db = db.Where("bank_id IN ?", params.BankIDs) } - if params.CustomerId != nil && params.SupplierId != nil { + customerIDs := params.CustomerIDs + supplierIDs := params.SupplierIDs + + if len(customerIDs) > 0 && len(supplierIDs) > 0 { db = db.Where( - "(party_type = ? AND party_id = ?) OR (party_type = ? AND party_id = ?)", - string(utils.PaymentPartyCustomer), *params.CustomerId, - string(utils.PaymentPartySupplier), *params.SupplierId, + "(party_type = ? AND party_id IN ?) OR (party_type = ? AND party_id IN ?)", + string(utils.PaymentPartyCustomer), customerIDs, + string(utils.PaymentPartySupplier), supplierIDs, ) - } else if params.CustomerId != nil { - db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartyCustomer), *params.CustomerId) - } else if params.SupplierId != nil { - db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartySupplier), *params.SupplierId) + } else if len(customerIDs) > 0 { + db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartyCustomer), customerIDs) + } else if len(supplierIDs) > 0 { + db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartySupplier), supplierIDs) } if startDate != nil { diff --git a/internal/modules/finance/transactions/validations/transaction.validation.go b/internal/modules/finance/transactions/validations/transaction.validation.go index f367dda1..7a71cb51 100644 --- a/internal/modules/finance/transactions/validations/transaction.validation.go +++ b/internal/modules/finance/transactions/validations/transaction.validation.go @@ -1,22 +1,22 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + Name string `json:"name" validate:"required_strict,min=3"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + Name *string `json:"name,omitempty" validate:"omitempty"` } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` - TransactionType string `query:"transaction_type" validate:"omitempty,max=50"` - BankId *uint `query:"bank_id" validate:"omitempty,number,gt=0"` - CustomerId *uint `query:"customer_id" validate:"omitempty,number,gt=0"` - SupplierId *uint `query:"supplier_id" validate:"omitempty,number,gt=0"` - SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"` - StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` - EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` + TransactionTypes []string `query:"transaction_types" validate:"omitempty,dive,max=50"` + BankIDs []uint `query:"bank_ids" validate:"omitempty,dive,gt=0"` + CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"` + SupplierIDs []uint `query:"supplier_ids" validate:"omitempty,dive,gt=0"` + SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` }