Merge branch 'dev/fifo-v2' of https://gitlab.com/mbugroup/lti-api into dev/fifo-v2

This commit is contained in:
ragilap
2026-03-06 11:46:31 +07:00
10 changed files with 573 additions and 74 deletions
@@ -0,0 +1,7 @@
BEGIN;
DELETE FROM fifo_stock_v2_flag_members
WHERE flag_name = 'AYAM'
AND flag_group_code = 'AYAM';
COMMIT;
@@ -0,0 +1,13 @@
BEGIN;
INSERT INTO fifo_stock_v2_flag_members(flag_name, flag_group_code, priority, is_active, created_at, updated_at)
VALUES
('AYAM', 'AYAM', 5, TRUE, NOW(), NOW())
ON CONFLICT (flag_name) DO UPDATE
SET
flag_group_code = EXCLUDED.flag_group_code,
priority = EXCLUDED.priority,
is_active = TRUE,
updated_at = NOW();
COMMIT;
@@ -32,6 +32,44 @@ func (r *ConstantRepositoryImpl) GetConstants() (map[string]interface{}, error)
} }
sort.Strings(flagList) sort.Strings(flagList)
productMainFlags := utils.ProductMainFlags()
productMainFlagValues := make([]string, len(productMainFlags))
for i, flag := range productMainFlags {
productMainFlagValues[i] = string(flag)
}
type productFlagOption struct {
Flag string `json:"flag"`
SubFlags []string `json:"sub_flags"`
AllowWithoutSubFlag bool `json:"allow_without_sub_flag"`
}
productOptions := utils.ProductFlagOptions()
productFlagOptions := make([]productFlagOption, 0, len(productOptions))
for _, option := range productOptions {
subFlags := make([]string, len(option.SubFlags))
for i, subFlag := range option.SubFlags {
subFlags[i] = string(subFlag)
}
productFlagOptions = append(productFlagOptions, productFlagOption{
Flag: string(option.Flag),
SubFlags: subFlags,
AllowWithoutSubFlag: option.AllowWithoutSubFlag,
})
}
productSubFlagToFlagRaw := utils.ProductSubFlagToFlag()
productSubFlagToFlag := make(map[string]string, len(productSubFlagToFlagRaw))
for subFlag, flag := range productSubFlagToFlagRaw {
productSubFlagToFlag[string(subFlag)] = string(flag)
}
legacyAliasesRaw := utils.LegacyFlagTypeAliases()
legacyAliases := make(map[string]string, len(legacyAliasesRaw))
for legacy, canonical := range legacyAliasesRaw {
legacyAliases[string(legacy)] = string(canonical)
}
type approvalStepConstant struct { type approvalStepConstant struct {
StepNumber uint16 `json:"step_number"` StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"` StepName string `json:"step_name"`
@@ -99,6 +137,12 @@ func (r *ConstantRepositoryImpl) GetConstants() (map[string]interface{}, error)
"adjustment": map[string]interface{}{ "adjustment": map[string]interface{}{
"transaction_subtypes": adjustmentSubtypesByType, "transaction_subtypes": adjustmentSubtypesByType,
}, },
"legacy_flag_aliases": legacyAliases,
"product_flag_mapping": map[string]interface{}{
"flags": productMainFlagValues,
"options": productFlagOptions,
"sub_flag_to_flag": productSubFlagToFlag,
},
"approval_workflows": approvalWorkflows, "approval_workflows": approvalWorkflows,
}, nil }, nil
} }
@@ -30,6 +30,14 @@ func (u *ProductController) GetAll(c *fiber.Ctx) error {
ProductCategoryID: c.QueryInt("product_category_id", 0), ProductCategoryID: c.QueryInt("product_category_id", 0),
} }
if isDepletionParam := c.Query("is_depletion", ""); isDepletionParam != "" {
value, err := strconv.ParseBool(isDepletionParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid is_depletion value")
}
query.IsDepletion = &value
}
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
} }
@@ -7,6 +7,7 @@ import (
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === DTO Structs === // === DTO Structs ===
@@ -17,6 +18,9 @@ type ProductRelationDTO struct {
ProductPrice float64 `gorm:"type:numeric(15,3);not null"` ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"` SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flag *string `json:"flag,omitempty"`
SubFlag *string `json:"sub_flag,omitempty"`
SubFlags *[]string `json:"sub_flags,omitempty"`
Flags *[]string `json:"flags,omitempty"` Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []ProductSupplierDTO `json:"suppliers"` Suppliers []ProductSupplierDTO `json:"suppliers"`
@@ -31,6 +35,9 @@ type ProductListDTO struct {
SellingPrice *float64 `json:"selling_price,omitempty"` SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,omitempty"` Tax *float64 `json:"tax,omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty"` ExpiryPeriod *int `json:"expiry_period,omitempty"`
Flag *string `json:"flag,omitempty"`
SubFlag *string `json:"sub_flag,omitempty"`
SubFlags []string `json:"sub_flags,omitempty"`
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
@@ -59,6 +66,13 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
for i, f := range e.Flags { for i, f := range e.Flags {
flags[i] = f.Name flags[i] = f.Name
} }
flag, subFlag, subFlags := resolveProductFlagAndSubFlags(flags)
var subFlagsRef *[]string
if len(subFlags) > 0 {
values := make([]string, len(subFlags))
copy(values, subFlags)
subFlagsRef = &values
}
var uomRef *uomDTO.UomRelationDTO var uomRef *uomDTO.UomRelationDTO
if e.Uom.Id != 0 { if e.Uom.Id != 0 {
@@ -77,6 +91,9 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
Name: e.Name, Name: e.Name,
ProductPrice: e.ProductPrice, ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice, SellingPrice: e.SellingPrice,
Flag: flag,
SubFlag: subFlag,
SubFlags: subFlagsRef,
Flags: &flags, Flags: &flags,
Uom: uomRef, Uom: uomRef,
ProductCategory: categoryRef, ProductCategory: categoryRef,
@@ -101,6 +118,7 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
for i, f := range e.Flags { for i, f := range e.Flags {
flags[i] = f.Name flags[i] = f.Name
} }
flag, subFlag, subFlags := resolveProductFlagAndSubFlags(flags)
var uomRef *uomDTO.UomRelationDTO var uomRef *uomDTO.UomRelationDTO
if e.Uom.Id != 0 { if e.Uom.Id != 0 {
@@ -111,6 +129,9 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
return ProductListDTO{ return ProductListDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
Flag: flag,
SubFlag: subFlag,
SubFlags: subFlags,
Flags: flags, Flags: flags,
Uom: uomRef, Uom: uomRef,
Brand: e.Brand, Brand: e.Brand,
@@ -141,6 +162,58 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
} }
} }
func resolveProductFlagAndSubFlags(flags []string) (*string, *string, []string) {
normalized := utils.NormalizeFlagTypes(flags)
if len(normalized) == 0 {
return nil, nil, nil
}
available := make(map[utils.FlagType]struct{}, len(normalized))
for _, flag := range normalized {
available[flag] = struct{}{}
}
var selectedFlag utils.FlagType
for _, mainFlag := range utils.ProductMainFlags() {
if _, ok := available[mainFlag]; ok {
selectedFlag = mainFlag
break
}
}
if selectedFlag == "" {
subToMain := utils.ProductSubFlagToFlag()
for _, flag := range normalized {
if parent, ok := subToMain[flag]; ok {
selectedFlag = parent
break
}
}
}
if selectedFlag == "" {
return nil, nil, nil
}
flag := string(selectedFlag)
var subFlag *string
subFlagValues := make([]string, 0)
subFlagsByMain := utils.ProductSubFlagsByFlag()
for _, sub := range subFlagsByMain[selectedFlag] {
if _, ok := available[sub]; ok {
subFlagValues = append(subFlagValues, string(sub))
}
}
if len(subFlagValues) > 0 {
first := subFlagValues[0]
subFlag = &first
}
return &flag, subFlag, subFlagValues
}
func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO { func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO {
if len(relations) == 0 { if len(relations) == 0 {
return make([]ProductSupplierDTO, 0) return make([]ProductSupplierDTO, 0)
@@ -31,6 +31,12 @@ type productService struct {
Repository repository.ProductRepository Repository repository.ProductRepository
} }
var depletionProductFlags = []string{
string(utils.FlagAyamAfkir),
string(utils.FlagAyamCulling),
string(utils.FlagAyamMati),
}
func normalizeProductFlags(raw []string) ([]string, error) { func normalizeProductFlags(raw []string) ([]string, error) {
normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupProduct) normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupProduct)
if len(invalid) > 0 { if len(invalid) > 0 {
@@ -41,6 +47,159 @@ func normalizeProductFlags(raw []string) ([]string, error) {
return utils.FlagTypesToStrings(normalized), nil return utils.FlagTypesToStrings(normalized), nil
} }
func productMainFlagOptionsString() []string {
mainFlags := utils.ProductMainFlags()
result := make([]string, len(mainFlags))
for i, flag := range mainFlags {
result[i] = string(flag)
}
return result
}
func productSubFlagOptionsString(flag utils.FlagType) []string {
subFlagsByFlag := utils.ProductSubFlagsByFlag()
subFlags := subFlagsByFlag[flag]
result := make([]string, len(subFlags))
for i, subFlag := range subFlags {
result[i] = string(subFlag)
}
return result
}
func normalizeStructuredSubFlagsInput(subFlagRaw *string, subFlagsRaw []string, hasSubFlagsField bool) ([]utils.FlagType, error) {
values := make([]string, 0, len(subFlagsRaw)+1)
if subFlagRaw != nil {
single := strings.TrimSpace(*subFlagRaw)
if single == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "sub_flag cannot be empty")
}
values = append(values, single)
}
if hasSubFlagsField {
for _, raw := range subFlagsRaw {
item := strings.TrimSpace(raw)
if item == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "sub_flags cannot contain empty value")
}
values = append(values, item)
}
}
if len(values) == 0 {
return nil, nil
}
return utils.NormalizeFlagTypes(values), nil
}
func resolveProductFlagsFromFlagInput(flagRaw *string, subFlagRaw *string, subFlagsRaw []string, hasSubFlagsField bool) ([]string, bool, error) {
if flagRaw == nil && subFlagRaw == nil && !hasSubFlagsField {
return nil, false, nil
}
if flagRaw == nil && (subFlagRaw != nil || hasSubFlagsField) {
return nil, false, fiber.NewError(fiber.StatusBadRequest, "flag is required when sub_flag/sub_flags is provided")
}
flagText := strings.TrimSpace(*flagRaw)
if flagText == "" {
return nil, false, fiber.NewError(fiber.StatusBadRequest, "flag cannot be empty")
}
flag := utils.CanonicalFlagType(flagText)
if !utils.IsProductMainFlag(flag) {
return nil, false, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Invalid product flag: %s. Allowed flags: %s", flagText, strings.Join(productMainFlagOptionsString(), ", ")),
)
}
out := []string{string(flag)}
normalizedSubFlags, err := normalizeStructuredSubFlagsInput(subFlagRaw, subFlagsRaw, hasSubFlagsField)
if err != nil {
return nil, false, err
}
if len(normalizedSubFlags) == 0 {
if !utils.ProductFlagAllowWithoutSubFlag(flag) {
return nil, false, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("sub_flag/sub_flags is required for flag %s", string(flag)),
)
}
normalizedOut, normalizeErr := normalizeProductFlags(out)
if normalizeErr != nil {
return nil, false, normalizeErr
}
return normalizedOut, true, nil
}
invalidSubFlags := make([]string, 0)
for _, subFlag := range normalizedSubFlags {
if !utils.IsValidProductSubFlag(flag, subFlag) {
invalidSubFlags = append(invalidSubFlags, string(subFlag))
}
}
if len(invalidSubFlags) > 0 {
return nil, false, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Invalid sub_flags %s for flag %s. Allowed sub_flags: %s", strings.Join(invalidSubFlags, ", "), string(flag), strings.Join(productSubFlagOptionsString(flag), ", ")),
)
}
out = append(out, utils.FlagTypesToStrings(normalizedSubFlags)...)
normalizedOut, normalizeErr := normalizeProductFlags(out)
if normalizeErr != nil {
return nil, false, normalizeErr
}
return normalizedOut, true, nil
}
func resolveCreateProductFlags(req *validation.Create) ([]string, error) {
hasStructuredInput := req.Flag != nil || req.SubFlag != nil || req.SubFlags != nil
if len(req.Flags) > 0 && hasStructuredInput {
return nil, fiber.NewError(fiber.StatusBadRequest, "Use either flags or flag/sub_flag/sub_flags, not both")
}
if len(req.Flags) > 0 {
return normalizeProductFlags(req.Flags)
}
flags, _, err := resolveProductFlagsFromFlagInput(req.Flag, req.SubFlag, req.SubFlags, req.SubFlags != nil)
return flags, err
}
func resolveUpdateProductFlags(req *validation.Update) (bool, []string, error) {
hasStructuredInput := req.Flag != nil || req.SubFlag != nil || req.SubFlags != nil
if req.Flags != nil {
if hasStructuredInput {
if len(*req.Flags) > 0 {
return false, nil, fiber.NewError(fiber.StatusBadRequest, "Use either flags or flag/sub_flag/sub_flags, not both")
}
} else {
flags, err := normalizeProductFlags(*req.Flags)
if err != nil {
return false, nil, err
}
return true, flags, nil
}
}
subFlagsRaw := make([]string, 0)
if req.SubFlags != nil {
subFlagsRaw = *req.SubFlags
}
flags, provided, err := resolveProductFlagsFromFlagInput(req.Flag, req.SubFlag, subFlagsRaw, req.SubFlags != nil)
if err != nil {
return false, nil, err
}
return provided, flags, nil
}
func NewProductService(repo repository.ProductRepository, validate *validator.Validate) ProductService { func NewProductService(repo repository.ProductRepository, validate *validator.Validate) ProductService {
return &productService{ return &productService{
Log: utils.Log, Log: utils.Log,
@@ -70,12 +229,32 @@ func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
products, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { products, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
// Depletion master products are system products and often stored with is_visible = false.
// When requested explicitly via is_depletion=true, include hidden records.
if params.IsDepletion == nil || !*params.IsDepletion {
db = db.Where("is_visible = ?", true) db = db.Where("is_visible = ?", true)
}
if params.Search != "" { if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%") db = db.Where("name ILIKE ?", "%"+params.Search+"%")
} }
if params.ProductCategoryID != 0 { if params.ProductCategoryID != 0 {
return db.Where("product_category_id = ?", params.ProductCategoryID) db = db.Where("product_category_id = ?", params.ProductCategoryID)
}
if params.IsDepletion != nil {
existsQuery := `
EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_type = ?
AND f.flagable_id = products.id
AND UPPER(f.name) IN ?
)
`
if *params.IsDepletion {
db = db.Where(existsQuery, entity.FlagableTypeProduct, depletionProductFlags)
} else {
db = db.Where("NOT "+existsQuery, entity.FlagableTypeProduct, depletionProductFlags)
}
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -177,7 +356,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
} }
productFlags, flagErr := normalizeProductFlags(req.Flags) productFlags, flagErr := resolveCreateProductFlags(req)
if flagErr != nil { if flagErr != nil {
return nil, flagErr return nil, flagErr
} }
@@ -337,14 +516,11 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
flagUpdate bool flagUpdate bool
flagValues []string flagValues []string
) )
if req.Flags != nil {
flagUpdate = true
var flagErr error var flagErr error
flagValues, flagErr = normalizeProductFlags(*req.Flags) flagUpdate, flagValues, flagErr = resolveUpdateProductFlags(req)
if flagErr != nil { if flagErr != nil {
return nil, flagErr return nil, flagErr
} }
}
if len(updateBody) == 0 && !supplierUpdate && !flagUpdate { if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
return s.GetOne(c, id) return s.GetOne(c, id)
@@ -16,6 +16,9 @@ type Create struct {
Tax *float64 `json:"tax,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flag *string `json:"flag,omitempty" validate:"omitempty,max=50"`
SubFlag *string `json:"sub_flag,omitempty" validate:"omitempty,max=50"`
SubFlags []string `json:"sub_flags,omitempty" validate:"omitempty,dive,max=50"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"` Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
} }
@@ -30,6 +33,9 @@ type Update struct {
Tax *float64 `json:"tax,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flag *string `json:"flag,omitempty" validate:"omitempty,max=50"`
SubFlag *string `json:"sub_flag,omitempty" validate:"omitempty,max=50"`
SubFlags *[]string `json:"sub_flags,omitempty" validate:"omitempty,dive,max=50"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"` Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
} }
@@ -38,4 +44,5 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1"` Limit int `query:"limit" validate:"omitempty,number,min=1"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
ProductCategoryID int `query:"product_category_id" validate:"omitempty,number,min=1"` ProductCategoryID int `query:"product_category_id" validate:"omitempty,number,min=1"`
IsDepletion *bool `query:"is_depletion" validate:"omitempty"`
} }
@@ -190,26 +190,27 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
} }
if productWarehouse.Product.Id != 0 { if productWarehouse.Product.Id != 0 {
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
var requiredFlag utils.FlagType if category != string(utils.ProjectFlockCategoryGrowing) && category != string(utils.ProjectFlockCategoryLaying) {
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
requiredFlag = utils.FlagDOC
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
requiredFlag = utils.FlagPullet
} else {
return nil, fmt.Errorf("invalid flock category for chickin") return nil, fmt.Errorf("invalid flock category for chickin")
} }
hasRequiredFlag := false hasAyamFlag := false
for _, flag := range productWarehouse.Product.Flags { for _, flag := range productWarehouse.Product.Flags {
if utils.FlagType(flag.Name) == requiredFlag { if utils.CanonicalFlagType(flag.Name) == utils.FlagAyam {
hasRequiredFlag = true hasAyamFlag = true
break break
} }
} }
if !hasRequiredFlag { if !hasAyamFlag {
return nil, fmt.Errorf("product warehouse %d cannot be used for %s chickin. Product must have %s flag (product ID: %d, warehouse ID: %d)", chickinReq.ProductWarehouseId, projectFlockKandang.ProjectFlock.Category, requiredFlag, productWarehouse.Product.Id, productWarehouse.Id) return nil, fmt.Errorf(
"product warehouse %d cannot be used for %s chickin. Product must have AYAM flag (or legacy alias DOC/PULLET/LAYER) (product ID: %d, warehouse ID: %d)",
chickinReq.ProductWarehouseId,
projectFlockKandang.ProjectFlock.Category,
productWarehouse.Product.Id,
productWarehouse.Id,
)
} }
} }
@@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -11,12 +12,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const (
chickinOutFunctionCode = "CHICKIN_OUT"
chickinUsableLane = "USABLE"
chickinSourceTable = "project_chickins"
)
func reflowChickinScope( func reflowChickinScope(
ctx context.Context, ctx context.Context,
fifoStockV2Svc commonSvc.FifoStockV2Service, fifoStockV2Svc commonSvc.FifoStockV2Service,
@@ -62,9 +57,6 @@ func resolveChickinFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB,
Select("rr.flag_group_code"). Select("rr.flag_group_code").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE"). Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE"). Where("rr.is_active = TRUE").
Where("rr.lane = ?", chickinUsableLane).
Where("rr.function_code = ?", chickinOutFunctionCode).
Where("rr.source_table = ?", chickinSourceTable).
Where(` Where(`
EXISTS ( EXISTS (
SELECT 1 SELECT 1
@@ -76,10 +68,13 @@ func resolveChickinFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB,
AND fm.flag_group_code = rr.flag_group_code AND fm.flag_group_code = rr.flag_group_code
) )
`, productWarehouseID, entity.FlagableTypeProduct). `, productWarehouseID, entity.FlagableTypeProduct).
Order("rr.id ASC"). Order("fg.priority ASC, rr.id ASC").
Limit(1). Limit(1).
Take(&selected).Error Take(&selected).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
return "", err return "", err
} }
+179 -4
View File
@@ -14,9 +14,17 @@ type FlagType string
type FlagGroup string type FlagGroup string
type ProductFlagOption struct {
Flag FlagType `json:"flag"`
SubFlags []FlagType `json:"sub_flags"`
AllowWithoutSubFlag bool `json:"allow_without_sub_flag"`
}
const ( const (
FlagIsActive FlagType = "IS_ACTIVE" FlagIsActive FlagType = "IS_ACTIVE"
FlagAyam FlagType = "AYAM"
FlagDOC FlagType = "DOC" FlagDOC FlagType = "DOC"
FlagPullet FlagType = "PULLET" FlagPullet FlagType = "PULLET"
FlagLayer FlagType = "LAYER" FlagLayer FlagType = "LAYER"
@@ -41,6 +49,8 @@ const (
FlagTelurPecah FlagType = "TELUR-PECAH" FlagTelurPecah FlagType = "TELUR-PECAH"
FlagTelurPutih FlagType = "TELUR-PUTIH" FlagTelurPutih FlagType = "TELUR-PUTIH"
FlagTelurRetak FlagType = "TELUR-RETAK" FlagTelurRetak FlagType = "TELUR-RETAK"
FlagTelurPapacal FlagType = "TELUR-PAPACAL"
FlagTelurJumbo FlagType = "TELUR-JUMBO"
) )
const ( const (
@@ -50,9 +60,10 @@ const (
var flagGroupOptions = map[FlagGroup][]FlagType{ var flagGroupOptions = map[FlagGroup][]FlagType{
FlagGroupProduct: { FlagGroupProduct: {
FlagDOC, FlagAyam,
FlagPullet, FlagAyamAfkir,
FlagLayer, FlagAyamCulling,
FlagAyamMati,
FlagPakan, FlagPakan,
FlagPreStarter, FlagPreStarter,
FlagStarter, FlagStarter,
@@ -61,12 +72,75 @@ var flagGroupOptions = map[FlagGroup][]FlagType{
FlagObat, FlagObat,
FlagVitamin, FlagVitamin,
FlagKimia, FlagKimia,
FlagTelur,
FlagTelurUtuh,
FlagTelurPecah,
FlagTelurPutih,
FlagTelurRetak,
FlagTelurPapacal,
FlagTelurJumbo,
}, },
FlagGroupNonstock: { FlagGroupNonstock: {
FlagEkspedisi, FlagEkspedisi,
}, },
} }
var productMainFlags = []FlagType{
FlagAyam,
FlagPakan,
FlagOVK,
FlagTelur,
}
var productSubFlagsByFlag = map[FlagType][]FlagType{
FlagAyam: {
FlagAyamAfkir,
FlagAyamCulling,
FlagAyamMati,
},
FlagPakan: {
FlagPreStarter,
FlagStarter,
FlagFinisher,
},
FlagOVK: {
FlagObat,
FlagVitamin,
FlagKimia,
},
FlagTelur: {
FlagTelurUtuh,
FlagTelurPutih,
FlagTelurRetak,
FlagTelurPecah,
FlagTelurPapacal,
FlagTelurJumbo,
},
}
var productSubFlagToFlag = func() map[FlagType]FlagType {
out := make(map[FlagType]FlagType)
for flag, subFlags := range productSubFlagsByFlag {
for _, subFlag := range subFlags {
out[subFlag] = flag
}
}
return out
}()
var productAllowWithoutSubFlagByFlag = map[FlagType]bool{
FlagAyam: true,
FlagPakan: false,
FlagOVK: false,
FlagTelur: false,
}
var legacyFlagTypeAliases = map[FlagType]FlagType{
FlagDOC: FlagAyam,
FlagPullet: FlagAyam,
FlagLayer: FlagAyam,
}
var allFlagTypes = func() map[FlagType]struct{} { var allFlagTypes = func() map[FlagType]struct{} {
m := map[FlagType]struct{}{ m := map[FlagType]struct{}{
FlagIsActive: {}, FlagIsActive: {},
@@ -83,6 +157,102 @@ func AllFlagTypes() map[FlagType]struct{} {
return allFlagTypes return allFlagTypes
} }
func canonicalizeFlagType(flag FlagType) FlagType {
if canonical, ok := legacyFlagTypeAliases[flag]; ok {
return canonical
}
return flag
}
func CanonicalFlagType(v string) FlagType {
normalized := FlagType(strings.ToUpper(strings.TrimSpace(v)))
if normalized == "" {
return ""
}
return canonicalizeFlagType(normalized)
}
func LegacyFlagTypeAliases() map[FlagType]FlagType {
out := make(map[FlagType]FlagType, len(legacyFlagTypeAliases))
for legacy, canonical := range legacyFlagTypeAliases {
out[legacy] = canonical
}
return out
}
func ProductMainFlags() []FlagType {
out := make([]FlagType, len(productMainFlags))
copy(out, productMainFlags)
return out
}
func ProductSubFlagsByFlag() map[FlagType][]FlagType {
out := make(map[FlagType][]FlagType, len(productSubFlagsByFlag))
for flag, subFlags := range productSubFlagsByFlag {
dup := make([]FlagType, len(subFlags))
copy(dup, subFlags)
out[flag] = dup
}
return out
}
func ProductSubFlagToFlag() map[FlagType]FlagType {
out := make(map[FlagType]FlagType, len(productSubFlagToFlag))
for subFlag, flag := range productSubFlagToFlag {
out[subFlag] = flag
}
return out
}
func ProductFlagOptions() []ProductFlagOption {
result := make([]ProductFlagOption, 0, len(productMainFlags))
for _, flag := range productMainFlags {
subFlags := productSubFlagsByFlag[flag]
dup := make([]FlagType, len(subFlags))
copy(dup, subFlags)
result = append(result, ProductFlagOption{
Flag: flag,
SubFlags: dup,
AllowWithoutSubFlag: productAllowWithoutSubFlagByFlag[flag],
})
}
return result
}
func ProductFlagAllowWithoutSubFlag(flag FlagType) bool {
canonical := canonicalizeFlagType(flag)
allow, ok := productAllowWithoutSubFlagByFlag[canonical]
if !ok {
return false
}
return allow
}
func IsProductMainFlag(flag FlagType) bool {
canonical := canonicalizeFlagType(flag)
for _, f := range productMainFlags {
if f == canonical {
return true
}
}
return false
}
func IsValidProductSubFlag(flag FlagType, subFlag FlagType) bool {
canonicalFlag := canonicalizeFlagType(flag)
canonicalSubFlag := canonicalizeFlagType(subFlag)
allowedSubFlags, ok := productSubFlagsByFlag[canonicalFlag]
if !ok {
return false
}
for _, allowed := range allowedSubFlags {
if allowed == canonicalSubFlag {
return true
}
}
return false
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// WarehouseType // WarehouseType
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -621,7 +791,11 @@ const (
// ------------------------------------------------------------------- // -------------------------------------------------------------------
func IsValidFlagType(v string) bool { func IsValidFlagType(v string) bool {
_, ok := allFlagTypes[FlagType(strings.ToUpper(strings.TrimSpace(v)))] flag := FlagType(strings.ToUpper(strings.TrimSpace(v)))
if _, ok := allFlagTypes[flag]; ok {
return true
}
_, ok := legacyFlagTypeAliases[flag]
return ok return ok
} }
@@ -667,6 +841,7 @@ func NormalizeFlagTypes(flags []string) []FlagType {
if normalized == "" { if normalized == "" {
continue continue
} }
normalized = canonicalizeFlagType(normalized)
if _, exists := seen[normalized]; exists { if _, exists := seen[normalized]; exists {
continue continue
} }