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/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/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/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..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) { @@ -99,6 +100,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 +135,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/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index 4bcbacca..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" @@ -76,16 +77,21 @@ 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"` + MarketingType string `json:"marketing_type"` + 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 +103,36 @@ 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 := math.Floor(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, + MarketingType: marketingType, + 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 +179,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 +219,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..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" @@ -10,13 +11,18 @@ 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"` + MarketingType string `json:"marketing_type"` + 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 +35,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 +43,33 @@ 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 := math.Floor(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, + MarketingType: marketingType, + 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 +86,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 6d9392a6..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") @@ -237,6 +239,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,25 +291,7 @@ 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 { - - 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 @@ -374,6 +364,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,25 +417,7 @@ 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 { - - 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 @@ -483,6 +461,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 df75fe82..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" @@ -69,6 +70,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") } @@ -103,6 +105,25 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, err } + // 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) if err != nil { return nil, err @@ -115,6 +136,12 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } for _, item := range req.MarketingProducts { + 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, "Unit konversi tidak valid") + } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err } @@ -149,6 +176,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e SoDate: soDate, SalesPersonId: req.SalesPersonId, Notes: req.Notes, + MarketingType: firstMarketingType, CreatedBy: actorID, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { @@ -161,10 +189,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, product.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 +234,23 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, err } + // 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 { return nil, err } @@ -234,6 +278,12 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if len(req.MarketingProducts) > 0 { for _, item := range req.MarketingProducts { + 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, "Unit konversi tidak valid") + } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err } @@ -281,6 +331,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if req.Notes != "" { updateBody["notes"] = req.Notes } + if len(req.MarketingProducts) > 0 { + updateBody["marketing_type"] = req.MarketingProducts[0].MarketingType + } if len(updateBody) > 0 { if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { @@ -309,38 +362,12 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u 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 { - totalPrice = rp.Qty * rp.UnitPrice - } else { - totalPrice = totalWeight * rp.UnitPrice - } + 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) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") } - if err == nil && deliveryProduct.Id != 0 { oldQty := old.Qty newQty := rp.Qty @@ -363,12 +390,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 +421,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, rp.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } @@ -399,7 +429,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 +711,21 @@ 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 { +func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, marketingType string, 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 - } - } - } - - totalWeight := rp.Qty * rp.AvgWeight - var totalPrice float64 - if isPakanOrOVK { - // PAKAN atau OVK: qty × unit_price - totalPrice = rp.Qty * rp.UnitPrice - } else { - // Produk lain: total_weight × unit_price - totalPrice = totalWeight * rp.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week) 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 @@ -744,3 +749,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 = math.Round(qty*unitPrice*100) / 100 + } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { + totalWeight = math.Round(qty*avgWeight*100) / 100 + totalPrice = math.Round(unitPrice*float64(*week)*qty*100) / 100 + } else { + totalWeight = math.Round(qty*avgWeight*100) / 100 + totalPrice = math.Round(totalWeight*unitPrice*100) / 100 + } + return totalWeight, totalPrice +} diff --git a/internal/modules/marketing/validations/salesorder.validation.go b/internal/modules/marketing/validations/salesorder.validation.go index b69da394..6d6b80b6 100644 --- a/internal/modules/marketing/validations/salesorder.validation.go +++ b/internal/modules/marketing/validations/salesorder.validation.go @@ -9,11 +9,15 @@ type Create struct { } 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"` + 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"` + 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:"omitempty,gt=0"` } type Update struct { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 3b8ee054..d395ad3c 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -212,6 +212,31 @@ 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" + ConvertionUnitQty ConvertionUnit = "QTY" +) + // ------------------------------------------------------------------- // ProjectFlockCategory // ------------------------------------------------------------------- @@ -610,6 +635,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, ConvertionUnitQty: + return true + } + return false +} + // example use // Recording helper