mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
fix: adjusment module depletion, chickin, recording refactor
This commit is contained in:
@@ -0,0 +1,381 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type productWarehouseScopeRow struct {
|
||||||
|
ProductWarehouseID uint `gorm:"column:product_warehouse_id"`
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
WarehouseID uint `gorm:"column:warehouse_id"`
|
||||||
|
ProjectFlockKandangID *uint `gorm:"column:project_flock_kandang_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type reflowTarget struct {
|
||||||
|
ProductWarehouseID uint
|
||||||
|
ProductID uint
|
||||||
|
WarehouseID uint
|
||||||
|
ProjectFlockKandangID *uint
|
||||||
|
FlagGroupCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
projectFlockKandangID uint
|
||||||
|
apply bool
|
||||||
|
asOfRaw string
|
||||||
|
includeShared bool
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.UintVar(&projectFlockKandangID, "project-flock-kandang-id", 0, "Project flock kandang ID (required)")
|
||||||
|
flag.BoolVar(&apply, "apply", false, "Apply reflow. If false, run as dry-run")
|
||||||
|
flag.StringVar(&asOfRaw, "as-of", "", "Optional AsOf boundary. Format: RFC3339 or YYYY-MM-DD")
|
||||||
|
flag.BoolVar(&includeShared, "include-shared", true, "Include product warehouses referenced by transactions in this PFK scope (including shared/non-bound product warehouses)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
log.Fatal("--project-flock-kandang-id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
asOf, err := parseAsOf(asOfRaw)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid --as-of: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
db := database.Connect(config.DBHost, config.DBName)
|
||||||
|
fifoStockV2Svc := commonSvc.NewFifoStockV2Service(db, nil)
|
||||||
|
|
||||||
|
exists, err := projectFlockKandangExists(ctx, db, projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to check project flock kandang: %v", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
log.Fatalf("project_flock_kandang_id %d not found", projectFlockKandangID)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedPWs, err := loadScopedProductWarehouses(ctx, db, projectFlockKandangID, includeShared)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to load scoped product warehouses: %v", err)
|
||||||
|
}
|
||||||
|
if len(scopedPWs) == 0 {
|
||||||
|
fmt.Printf("Mode: %s\n", modeLabel(apply))
|
||||||
|
fmt.Printf("Scope: project_flock_kandang_id=%d\n", projectFlockKandangID)
|
||||||
|
fmt.Println("No product warehouse found in scope")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targets := make([]reflowTarget, 0, len(scopedPWs))
|
||||||
|
skippedPW := 0
|
||||||
|
failedResolve := 0
|
||||||
|
|
||||||
|
for _, pw := range scopedPWs {
|
||||||
|
flagGroups, err := resolveFlagGroupsByProductWarehouse(ctx, db, pw.ProductWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("FAIL pw=%d error=resolve flag groups: %v\n", pw.ProductWarehouseID, err)
|
||||||
|
failedResolve++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(flagGroups) == 0 {
|
||||||
|
fmt.Printf("SKIP pw=%d reason=no active fifo v2 route by product flag\n", pw.ProductWarehouseID)
|
||||||
|
skippedPW++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, group := range flagGroups {
|
||||||
|
targets = append(targets, reflowTarget{
|
||||||
|
ProductWarehouseID: pw.ProductWarehouseID,
|
||||||
|
ProductID: pw.ProductID,
|
||||||
|
WarehouseID: pw.WarehouseID,
|
||||||
|
ProjectFlockKandangID: pw.ProjectFlockKandangID,
|
||||||
|
FlagGroupCode: group,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(targets, func(i, j int) bool {
|
||||||
|
if targets[i].ProductWarehouseID == targets[j].ProductWarehouseID {
|
||||||
|
return targets[i].FlagGroupCode < targets[j].FlagGroupCode
|
||||||
|
}
|
||||||
|
return targets[i].ProductWarehouseID < targets[j].ProductWarehouseID
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("Mode: %s\n", modeLabel(apply))
|
||||||
|
fmt.Printf("Scope: project_flock_kandang_id=%d include_shared=%t\n", projectFlockKandangID, includeShared)
|
||||||
|
if asOf != nil {
|
||||||
|
fmt.Printf("AsOf: %s\n", asOf.UTC().Format(time.RFC3339))
|
||||||
|
} else {
|
||||||
|
fmt.Println("AsOf: <nil> (full timeline)")
|
||||||
|
}
|
||||||
|
fmt.Printf("Product warehouses in scope: %d\n", len(scopedPWs))
|
||||||
|
fmt.Printf("Planned reflow targets: %d\n\n", len(targets))
|
||||||
|
|
||||||
|
for _, target := range targets {
|
||||||
|
fmt.Printf(
|
||||||
|
"PLAN pw=%d product=%d warehouse=%d pw_pfk=%s flag_group=%s\n",
|
||||||
|
target.ProductWarehouseID,
|
||||||
|
target.ProductID,
|
||||||
|
target.WarehouseID,
|
||||||
|
displayOptionalUint(target.ProjectFlockKandangID),
|
||||||
|
target.FlagGroupCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=0 failed_apply=0\n", len(targets), skippedPW, failedResolve)
|
||||||
|
if failedResolve > 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successApply := 0
|
||||||
|
failedApply := 0
|
||||||
|
for idx, target := range targets {
|
||||||
|
req := commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: target.FlagGroupCode,
|
||||||
|
ProductWarehouseID: target.ProductWarehouseID,
|
||||||
|
AsOf: asOf,
|
||||||
|
IdempotencyKey: fmt.Sprintf(
|
||||||
|
"manual-pfk-reflow-%d-%d-%s-%d-%d",
|
||||||
|
projectFlockKandangID,
|
||||||
|
target.ProductWarehouseID,
|
||||||
|
strings.ToUpper(strings.TrimSpace(target.FlagGroupCode)),
|
||||||
|
time.Now().UnixNano(),
|
||||||
|
idx,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := fifoStockV2Svc.Reflow(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("FAIL pw=%d flag_group=%s error=%v\n", target.ProductWarehouseID, target.FlagGroupCode, err)
|
||||||
|
failedApply++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"DONE pw=%d flag_group=%s rollback=%.3f allocate=%.3f pending=%.3f processed_usable=%d\n",
|
||||||
|
target.ProductWarehouseID,
|
||||||
|
target.FlagGroupCode,
|
||||||
|
res.Rollback.ReleasedQty,
|
||||||
|
res.Allocate.AllocatedQty,
|
||||||
|
res.Allocate.PendingQty,
|
||||||
|
res.ProcessedUsables,
|
||||||
|
)
|
||||||
|
successApply++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(
|
||||||
|
"Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=%d failed_apply=%d\n",
|
||||||
|
len(targets),
|
||||||
|
skippedPW,
|
||||||
|
failedResolve,
|
||||||
|
successApply,
|
||||||
|
failedApply,
|
||||||
|
)
|
||||||
|
if failedResolve > 0 || failedApply > 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAsOf(raw string) (*time.Time, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts := []string{
|
||||||
|
time.RFC3339Nano,
|
||||||
|
time.RFC3339,
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, layout := range layouts {
|
||||||
|
parsed, err := time.Parse(layout, raw)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if layout == "2006-01-02" {
|
||||||
|
endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), time.UTC)
|
||||||
|
return &endOfDay, nil
|
||||||
|
}
|
||||||
|
asOf := parsed.UTC()
|
||||||
|
return &asOf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unsupported format %q", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func modeLabel(apply bool) string {
|
||||||
|
if apply {
|
||||||
|
return "APPLY"
|
||||||
|
}
|
||||||
|
return "DRY-RUN"
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayOptionalUint(v *uint) string {
|
||||||
|
if v == nil {
|
||||||
|
return "NULL"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectFlockKandangExists(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Where("id = ?", projectFlockKandangID).
|
||||||
|
Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadScopedProductWarehouses(ctx context.Context, db *gorm.DB, projectFlockKandangID uint, includeShared bool) ([]productWarehouseScopeRow, error) {
|
||||||
|
if !includeShared {
|
||||||
|
var rows []productWarehouseScopeRow
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Table("product_warehouses").
|
||||||
|
Select("id AS product_warehouse_id, product_id, warehouse_id, project_flock_kandang_id").
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Order("id ASC").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
WITH scoped_pw AS (
|
||||||
|
SELECT pw.id AS product_warehouse_id
|
||||||
|
FROM product_warehouses pw
|
||||||
|
WHERE pw.project_flock_kandang_id = ?
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT pc.product_warehouse_id
|
||||||
|
FROM project_chickins pc
|
||||||
|
WHERE pc.project_flock_kandang_id = ?
|
||||||
|
AND pc.deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT rs.product_warehouse_id
|
||||||
|
FROM recordings r
|
||||||
|
JOIN recording_stocks rs ON rs.recording_id = r.id
|
||||||
|
WHERE r.project_flock_kandangs_id = ?
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT rd.product_warehouse_id
|
||||||
|
FROM recordings r
|
||||||
|
JOIN recording_depletions rd ON rd.recording_id = r.id
|
||||||
|
WHERE r.project_flock_kandangs_id = ?
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT rd.source_product_warehouse_id
|
||||||
|
FROM recordings r
|
||||||
|
JOIN recording_depletions rd ON rd.recording_id = r.id
|
||||||
|
WHERE r.project_flock_kandangs_id = ?
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
AND rd.source_product_warehouse_id IS NOT NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT re.product_warehouse_id
|
||||||
|
FROM recordings r
|
||||||
|
JOIN recording_eggs re ON re.recording_id = r.id
|
||||||
|
WHERE r.project_flock_kandangs_id = ?
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT lts.product_warehouse_id
|
||||||
|
FROM laying_transfer_sources lts
|
||||||
|
WHERE lts.source_project_flock_kandang_id = ?
|
||||||
|
AND lts.deleted_at IS NULL
|
||||||
|
AND lts.product_warehouse_id IS NOT NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT ltt.product_warehouse_id
|
||||||
|
FROM laying_transfer_targets ltt
|
||||||
|
WHERE ltt.target_project_flock_kandang_id = ?
|
||||||
|
AND ltt.deleted_at IS NULL
|
||||||
|
AND ltt.product_warehouse_id IS NOT NULL
|
||||||
|
|
||||||
|
UNION
|
||||||
|
SELECT pi.product_warehouse_id
|
||||||
|
FROM purchase_items pi
|
||||||
|
WHERE pi.project_flock_kandang_id = ?
|
||||||
|
AND pi.product_warehouse_id IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT DISTINCT
|
||||||
|
pw.id AS product_warehouse_id,
|
||||||
|
pw.product_id,
|
||||||
|
pw.warehouse_id,
|
||||||
|
pw.project_flock_kandang_id
|
||||||
|
FROM scoped_pw s
|
||||||
|
JOIN product_warehouses pw ON pw.id = s.product_warehouse_id
|
||||||
|
ORDER BY pw.id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
var rows []productWarehouseScopeRow
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Raw(
|
||||||
|
query,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
projectFlockKandangID,
|
||||||
|
).
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveFlagGroupsByProductWarehouse(ctx context.Context, db *gorm.DB, productWarehouseID uint) ([]string, error) {
|
||||||
|
var groups []string
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Table("fifo_stock_v2_route_rules rr").
|
||||||
|
Select("DISTINCT rr.flag_group_code").
|
||||||
|
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(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM product_warehouses pw
|
||||||
|
JOIN flags f ON f.flagable_id = pw.product_id
|
||||||
|
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
|
||||||
|
WHERE pw.id = ?
|
||||||
|
AND f.flagable_type = ?
|
||||||
|
AND fm.flag_group_code = rr.flag_group_code
|
||||||
|
)
|
||||||
|
`, productWarehouseID, entity.FlagableTypeProduct).
|
||||||
|
Order("rr.flag_group_code ASC").
|
||||||
|
Scan(&groups).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
@@ -487,16 +487,12 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
asOf := usableRow.SortAt
|
|
||||||
if req.AsOf != nil && asOf.Before(*req.AsOf) {
|
|
||||||
asOf = *req.AsOf
|
|
||||||
}
|
|
||||||
allocateRes, allocateErr := s.allocateInternal(ctx, tx, AllocateRequest{
|
allocateRes, allocateErr := s.allocateInternal(ctx, tx, AllocateRequest{
|
||||||
FlagGroupCode: req.FlagGroupCode,
|
FlagGroupCode: req.FlagGroupCode,
|
||||||
ProductWarehouseID: req.ProductWarehouseID,
|
ProductWarehouseID: req.ProductWarehouseID,
|
||||||
Usable: usableRow.Ref,
|
Usable: usableRow.Ref,
|
||||||
NeedQty: desiredQty,
|
NeedQty: desiredQty,
|
||||||
AsOf: &asOf,
|
AsOf: nil,
|
||||||
})
|
})
|
||||||
if allocateErr != nil {
|
if allocateErr != nil {
|
||||||
err = allocateErr
|
err = allocateErr
|
||||||
|
|||||||
@@ -151,19 +151,29 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
|
|||||||
usedExpr := "0::numeric"
|
usedExpr := "0::numeric"
|
||||||
pendingExpr := "0::numeric"
|
pendingExpr := "0::numeric"
|
||||||
availableExpr := baseQtyExpr
|
availableExpr := baseQtyExpr
|
||||||
extraArgs := make([]any, 0, 1)
|
extraArgs := make([]any, 0, 2)
|
||||||
|
whereExtraArgs := make([]any, 0, 1)
|
||||||
|
|
||||||
if req.Lane == LaneStockable {
|
if req.Lane == LaneStockable {
|
||||||
if rule.UsedQuantityCol != nil && strings.TrimSpace(*rule.UsedQuantityCol) != "" {
|
if rule.UsedQuantityCol != nil && strings.TrimSpace(*rule.UsedQuantityCol) != "" {
|
||||||
usedCol, _ := mustSafeIdentifier(*rule.UsedQuantityCol)
|
usedCol, _ := mustSafeIdentifier(*rule.UsedQuantityCol)
|
||||||
usedExpr = fmt.Sprintf("COALESCE(src.%s,0)::numeric", usedCol)
|
usedExpr = fmt.Sprintf("COALESCE(src.%s,0)::numeric", usedCol)
|
||||||
} else {
|
} else {
|
||||||
|
// NOTE:
|
||||||
|
// usedExpr is referenced twice in the generated SELECT:
|
||||||
|
// 1) as used_quantity
|
||||||
|
// 2) inside available_quantity = base - usedExpr
|
||||||
|
// plus once in stockable WHERE clause via availableExpr > 0.
|
||||||
|
// We split the args because the WHERE placeholder order appears
|
||||||
|
// after product/flag filter placeholders in the final SQL.
|
||||||
usedExpr = fmt.Sprintf(
|
usedExpr = fmt.Sprintf(
|
||||||
"(SELECT COALESCE(SUM(sa.qty),0)::numeric FROM stock_allocations sa WHERE sa.stockable_type = ? AND sa.stockable_id = src.%s AND sa.status = '%s')",
|
"(SELECT COALESCE(SUM(sa.qty),0)::numeric FROM stock_allocations sa WHERE sa.stockable_type = ? AND sa.stockable_id = src.%s AND sa.status = '%s')",
|
||||||
sourceIDCol,
|
sourceIDCol,
|
||||||
activeAllocationStatus(),
|
activeAllocationStatus(),
|
||||||
)
|
)
|
||||||
extraArgs = append(extraArgs, rule.LegacyTypeKey)
|
extraArgs = append(extraArgs, rule.LegacyTypeKey)
|
||||||
|
extraArgs = append(extraArgs, rule.LegacyTypeKey)
|
||||||
|
whereExtraArgs = append(whereExtraArgs, rule.LegacyTypeKey)
|
||||||
}
|
}
|
||||||
availableExpr = fmt.Sprintf("(%s - %s)", baseQtyExpr, usedExpr)
|
availableExpr = fmt.Sprintf("(%s - %s)", baseQtyExpr, usedExpr)
|
||||||
} else {
|
} else {
|
||||||
@@ -179,6 +189,12 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
|
|||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
functionCodeExpr := "?::text"
|
||||||
|
functionCodeArgs := []any{rule.FunctionCode}
|
||||||
|
if rule.SourceTable == "adjustment_stocks" {
|
||||||
|
functionCodeExpr = "COALESCE(NULLIF(src.function_code,''), ?::text)"
|
||||||
|
}
|
||||||
|
|
||||||
whereParts := []string{
|
whereParts := []string{
|
||||||
fmt.Sprintf("src.%s = ?", productWarehouseCol),
|
fmt.Sprintf("src.%s = ?", productWarehouseCol),
|
||||||
fmt.Sprintf(`EXISTS (
|
fmt.Sprintf(`EXISTS (
|
||||||
@@ -209,7 +225,7 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
|
|||||||
SELECT
|
SELECT
|
||||||
?::text AS source_table,
|
?::text AS source_table,
|
||||||
?::text AS legacy_type_key,
|
?::text AS legacy_type_key,
|
||||||
?::text AS function_code,
|
%s AS function_code,
|
||||||
src.%s AS source_id,
|
src.%s AS source_id,
|
||||||
src.%s AS product_warehouse_id,
|
src.%s AS product_warehouse_id,
|
||||||
%s AS sort_at,
|
%s AS sort_at,
|
||||||
@@ -221,20 +237,21 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
|
|||||||
FROM %s src
|
FROM %s src
|
||||||
%s
|
%s
|
||||||
WHERE %s
|
WHERE %s
|
||||||
`, sourceIDCol, productWarehouseCol, sortExpr, baseQtyExpr, usedExpr, pendingExpr, availableExpr, sourceTable, joinClause, strings.Join(whereParts, " AND "))
|
`, functionCodeExpr, sourceIDCol, productWarehouseCol, sortExpr, baseQtyExpr, usedExpr, pendingExpr, availableExpr, sourceTable, joinClause, strings.Join(whereParts, " AND "))
|
||||||
|
|
||||||
args := []any{
|
args := []any{
|
||||||
rule.SourceTable,
|
rule.SourceTable,
|
||||||
rule.LegacyTypeKey,
|
rule.LegacyTypeKey,
|
||||||
rule.FunctionCode,
|
|
||||||
trait.SortPriority,
|
|
||||||
}
|
}
|
||||||
|
args = append(args, functionCodeArgs...)
|
||||||
|
args = append(args, trait.SortPriority)
|
||||||
args = append(args, extraArgs...)
|
args = append(args, extraArgs...)
|
||||||
args = append(args,
|
args = append(args,
|
||||||
req.ProductWarehouseID,
|
req.ProductWarehouseID,
|
||||||
entity.FlagableTypeProduct,
|
entity.FlagableTypeProduct,
|
||||||
req.FlagGroupCode,
|
req.FlagGroupCode,
|
||||||
)
|
)
|
||||||
|
args = append(args, whereExtraArgs...)
|
||||||
|
|
||||||
if req.AsOf != nil {
|
if req.AsOf != nil {
|
||||||
args = append(args, *req.AsOf)
|
args = append(args, *req.AsOf)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Restore CHICKIN route if rollback is required.
|
||||||
|
-- NOTE: released PROJECT_CHICKIN allocations are not restored by this down migration.
|
||||||
|
UPDATE fifo_stock_v2_route_rules
|
||||||
|
SET is_active = TRUE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE flag_group_code = 'AYAM'
|
||||||
|
AND lane = 'USABLE'
|
||||||
|
AND function_code = 'CHICKIN_OUT'
|
||||||
|
AND source_table = 'project_chickins';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Disable CHICKIN as FIFO USABLE so chick-in acts as business tagging/conversion,
|
||||||
|
-- not physical stock consumption.
|
||||||
|
UPDATE fifo_stock_v2_route_rules
|
||||||
|
SET is_active = FALSE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE flag_group_code = 'AYAM'
|
||||||
|
AND lane = 'USABLE'
|
||||||
|
AND function_code = 'CHICKIN_OUT'
|
||||||
|
AND source_table = 'project_chickins'
|
||||||
|
AND is_active = TRUE;
|
||||||
|
|
||||||
|
-- Release existing active allocations created by PROJECT_CHICKIN
|
||||||
|
-- and return warehouse qty back.
|
||||||
|
WITH released AS (
|
||||||
|
UPDATE stock_allocations
|
||||||
|
SET status = 'RELEASED',
|
||||||
|
released_at = COALESCE(released_at, NOW()),
|
||||||
|
updated_at = NOW(),
|
||||||
|
note = CASE
|
||||||
|
WHEN COALESCE(note, '') = '' THEN 'fifo_v2_chickin_conversion_release'
|
||||||
|
ELSE note || '; fifo_v2_chickin_conversion_release'
|
||||||
|
END
|
||||||
|
WHERE usable_type = 'PROJECT_CHICKIN'
|
||||||
|
AND status = 'ACTIVE'
|
||||||
|
RETURNING product_warehouse_id, qty
|
||||||
|
),
|
||||||
|
pw_delta AS (
|
||||||
|
SELECT product_warehouse_id, COALESCE(SUM(qty), 0) AS qty_delta
|
||||||
|
FROM released
|
||||||
|
GROUP BY product_warehouse_id
|
||||||
|
)
|
||||||
|
UPDATE product_warehouses pw
|
||||||
|
SET qty = COALESCE(pw.qty, 0) + d.qty_delta
|
||||||
|
FROM pw_delta d
|
||||||
|
WHERE pw.id = d.product_warehouse_id;
|
||||||
|
|
||||||
|
-- Resync stockable total_used columns from remaining ACTIVE allocations.
|
||||||
|
|
||||||
|
-- purchase_items (PURCHASE_ITEMS)
|
||||||
|
UPDATE purchase_items pi
|
||||||
|
SET total_used = COALESCE(a.used, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, SUM(qty) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE status = 'ACTIVE'
|
||||||
|
AND stockable_type = 'PURCHASE_ITEMS'
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) a
|
||||||
|
WHERE pi.id = a.stockable_id;
|
||||||
|
|
||||||
|
UPDATE purchase_items pi
|
||||||
|
SET total_used = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_allocations sa
|
||||||
|
WHERE sa.status = 'ACTIVE'
|
||||||
|
AND sa.stockable_type = 'PURCHASE_ITEMS'
|
||||||
|
AND sa.stockable_id = pi.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- stock_transfer_details (STOCK_TRANSFER_IN)
|
||||||
|
UPDATE stock_transfer_details std
|
||||||
|
SET total_used = COALESCE(a.used, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, SUM(qty) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE status = 'ACTIVE'
|
||||||
|
AND stockable_type = 'STOCK_TRANSFER_IN'
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) a
|
||||||
|
WHERE std.id = a.stockable_id;
|
||||||
|
|
||||||
|
UPDATE stock_transfer_details std
|
||||||
|
SET total_used = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_allocations sa
|
||||||
|
WHERE sa.status = 'ACTIVE'
|
||||||
|
AND sa.stockable_type = 'STOCK_TRANSFER_IN'
|
||||||
|
AND sa.stockable_id = std.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- adjustment_stocks (ADJUSTMENT_IN)
|
||||||
|
UPDATE adjustment_stocks ast
|
||||||
|
SET total_used = COALESCE(a.used, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, SUM(qty) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE status = 'ACTIVE'
|
||||||
|
AND stockable_type = 'ADJUSTMENT_IN'
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) a
|
||||||
|
WHERE ast.id = a.stockable_id;
|
||||||
|
|
||||||
|
UPDATE adjustment_stocks ast
|
||||||
|
SET total_used = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_allocations sa
|
||||||
|
WHERE sa.status = 'ACTIVE'
|
||||||
|
AND sa.stockable_type = 'ADJUSTMENT_IN'
|
||||||
|
AND sa.stockable_id = ast.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- laying_transfer_targets (TRANSFERTOLAYING_IN)
|
||||||
|
UPDATE laying_transfer_targets ltt
|
||||||
|
SET total_used = COALESCE(a.used, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, SUM(qty) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE status = 'ACTIVE'
|
||||||
|
AND stockable_type = 'TRANSFERTOLAYING_IN'
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) a
|
||||||
|
WHERE ltt.id = a.stockable_id;
|
||||||
|
|
||||||
|
UPDATE laying_transfer_targets ltt
|
||||||
|
SET total_used = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_allocations sa
|
||||||
|
WHERE sa.status = 'ACTIVE'
|
||||||
|
AND sa.stockable_type = 'TRANSFERTOLAYING_IN'
|
||||||
|
AND sa.stockable_id = ltt.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- recording_eggs (RECORDING_EGG)
|
||||||
|
UPDATE recording_eggs re
|
||||||
|
SET total_used = COALESCE(a.used, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, SUM(qty) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE status = 'ACTIVE'
|
||||||
|
AND stockable_type = 'RECORDING_EGG'
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) a
|
||||||
|
WHERE re.id = a.stockable_id;
|
||||||
|
|
||||||
|
UPDATE recording_eggs re
|
||||||
|
SET total_used = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_allocations sa
|
||||||
|
WHERE sa.status = 'ACTIVE'
|
||||||
|
AND sa.stockable_type = 'RECORDING_EGG'
|
||||||
|
AND sa.stockable_id = re.id
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -46,6 +46,7 @@ type adjustmentService struct {
|
|||||||
const (
|
const (
|
||||||
adjustmentLaneStockable = "STOCKABLE"
|
adjustmentLaneStockable = "STOCKABLE"
|
||||||
adjustmentLaneUsable = "USABLE"
|
adjustmentLaneUsable = "USABLE"
|
||||||
|
flagGroupAyam = "AYAM"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAdjustmentService(
|
func NewAdjustmentService(
|
||||||
@@ -129,8 +130,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
if functionCode == "" {
|
if functionCode == "" {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype is required")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype is required")
|
||||||
}
|
}
|
||||||
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) {
|
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut) {
|
||||||
functionCode = string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut)
|
return nil, fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
"RECORDING_DEPLETION_OUT tidak boleh diinput manual. Gunakan RECORDING_DEPLETION_IN, sistem akan otomatis membuat depletion-out AYAM",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
warehouseID, err := s.resolveWarehouseID(c.Context(), req)
|
warehouseID, err := s.resolveWarehouseID(c.Context(), req)
|
||||||
@@ -211,6 +215,133 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) {
|
||||||
|
if routeMeta.Lane != adjustmentLaneStockable {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Transaction subtype depletion in harus lane STOCKABLE")
|
||||||
|
}
|
||||||
|
if projectFlockKandangID == nil || *projectFlockKandangID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id aktif wajib tersedia untuk depletion conversion")
|
||||||
|
}
|
||||||
|
if s.FifoStockV2Svc == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePW, err := s.resolveAyamSourceProductWarehouse(ctx, tx, warehouseID, *projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
[]uint{productWarehouse.Id, sourcePW.Id},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceRoute, err := s.resolveRouteByFunctionCode(
|
||||||
|
ctx,
|
||||||
|
sourcePW.ProductId,
|
||||||
|
string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sourceRoute.Lane != adjustmentLaneUsable {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Route depletion out untuk produk AYAM tidak valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sourceAdjustment := &entity.AdjustmentStock{
|
||||||
|
ProductWarehouseId: sourcePW.Id,
|
||||||
|
TransactionType: transactionType,
|
||||||
|
FunctionCode: sourceRoute.FunctionCode,
|
||||||
|
UsageQty: qty,
|
||||||
|
Price: req.Price,
|
||||||
|
GrandTotal: grandTotal,
|
||||||
|
AdjNumber: sourceCode,
|
||||||
|
}
|
||||||
|
if err := adjustmentStockRepoTX.CreateOne(ctx, sourceAdjustment, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion source adjustment stock record")
|
||||||
|
}
|
||||||
|
|
||||||
|
destCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
destinationAdjustment := &entity.AdjustmentStock{
|
||||||
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
|
TransactionType: transactionType,
|
||||||
|
FunctionCode: routeMeta.FunctionCode,
|
||||||
|
TotalQty: qty,
|
||||||
|
Price: req.Price,
|
||||||
|
GrandTotal: grandTotal,
|
||||||
|
AdjNumber: destCode,
|
||||||
|
}
|
||||||
|
if err := adjustmentStockRepoTX.CreateOne(ctx, destinationAdjustment, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion destination adjustment stock record")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceAsOf := sourceAdjustment.CreatedAt
|
||||||
|
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: sourceRoute.FlagGroupCode,
|
||||||
|
ProductWarehouseID: sourcePW.Id,
|
||||||
|
AsOf: &sourceAsOf,
|
||||||
|
Tx: tx,
|
||||||
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-out AYAM via FIFO v2: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationAsOf := destinationAdjustment.CreatedAt
|
||||||
|
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: routeMeta.FlagGroupCode,
|
||||||
|
ProductWarehouseID: destinationAdjustment.ProductWarehouseId,
|
||||||
|
AsOf: &destinationAsOf,
|
||||||
|
Tx: tx,
|
||||||
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-in destination via FIFO v2: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshedSource, err := adjustmentStockRepoTX.GetByID(ctx, sourceAdjustment.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion source adjustment stock")
|
||||||
|
}
|
||||||
|
refreshedDestination, err := adjustmentStockRepoTX.GetByID(ctx, destinationAdjustment.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion destination adjustment stock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.createAdjustmentStockLog(
|
||||||
|
ctx,
|
||||||
|
stockLogRepoTX,
|
||||||
|
refreshedSource.Id,
|
||||||
|
refreshedSource.ProductWarehouseId,
|
||||||
|
note,
|
||||||
|
actorID,
|
||||||
|
0,
|
||||||
|
refreshedSource.UsageQty+refreshedSource.PendingQty,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.createAdjustmentStockLog(
|
||||||
|
ctx,
|
||||||
|
stockLogRepoTX,
|
||||||
|
refreshedDestination.Id,
|
||||||
|
refreshedDestination.ProductWarehouseId,
|
||||||
|
note,
|
||||||
|
actorID,
|
||||||
|
refreshedDestination.TotalQty,
|
||||||
|
0,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAdjustmentStockId = destinationAdjustment.Id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
adjustmentStock := &entity.AdjustmentStock{
|
adjustmentStock := &entity.AdjustmentStock{
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
TransactionType: transactionType,
|
TransactionType: transactionType,
|
||||||
@@ -264,29 +395,16 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
decreaseQty = refreshedAdjustment.UsageQty
|
decreaseQty = refreshedAdjustment.UsageQty
|
||||||
}
|
}
|
||||||
|
|
||||||
stockLogs, err := stockLogRepoTX.GetByProductWarehouse(ctx, productWarehouse.Id, 1)
|
if err := s.createAdjustmentStockLog(
|
||||||
if err != nil {
|
ctx,
|
||||||
s.Log.Errorf("Failed to get stock logs: %+v", err)
|
stockLogRepoTX,
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
adjustmentStock.Id,
|
||||||
}
|
productWarehouse.Id,
|
||||||
|
note,
|
||||||
currentStock := 0.0
|
actorID,
|
||||||
if len(stockLogs) > 0 {
|
increaseQty,
|
||||||
currentStock = stockLogs[0].Stock
|
decreaseQty,
|
||||||
}
|
); err != nil {
|
||||||
|
|
||||||
newLog := &entity.StockLog{
|
|
||||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
|
||||||
LoggableId: adjustmentStock.Id,
|
|
||||||
Notes: note,
|
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
Increase: increaseQty,
|
|
||||||
Decrease: decreaseQty,
|
|
||||||
Stock: currentStock + increaseQty - decreaseQty,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := stockLogRepoTX.CreateOne(ctx, newLog, nil); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +535,88 @@ func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context,
|
|||||||
return uint(projectFlockKandang.Id), nil
|
return uint(projectFlockKandang.Id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *adjustmentService) resolveAyamSourceProductWarehouse(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
warehouseID uint,
|
||||||
|
projectFlockKandangID uint,
|
||||||
|
) (*entity.ProductWarehouse, error) {
|
||||||
|
if tx == nil {
|
||||||
|
return nil, fmt.Errorf("transaction is required")
|
||||||
|
}
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id tidak valid untuk depletion conversion")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourcePW entity.ProductWarehouse
|
||||||
|
err := tx.WithContext(ctx).
|
||||||
|
Model(&entity.ProductWarehouse{}).
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM flags f
|
||||||
|
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
|
||||||
|
WHERE f.flagable_type = ?
|
||||||
|
AND f.flagable_id = product_warehouses.product_id
|
||||||
|
AND fm.flag_group_code = ?
|
||||||
|
)
|
||||||
|
`, entity.FlagableTypeProduct, flagGroupAyam).
|
||||||
|
Order(gorm.Expr("CASE WHEN warehouse_id = ? THEN 0 ELSE 1 END ASC", warehouseID)).
|
||||||
|
Order("id ASC").
|
||||||
|
Take(&sourcePW).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Produk sumber AYAM pada project flock kandang yang sama tidak ditemukan")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sourcePW, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *adjustmentService) createAdjustmentStockLog(
|
||||||
|
ctx context.Context,
|
||||||
|
stockLogRepo stockLogsRepo.StockLogRepository,
|
||||||
|
adjustmentID uint,
|
||||||
|
productWarehouseID uint,
|
||||||
|
note string,
|
||||||
|
actorID uint,
|
||||||
|
increaseQty float64,
|
||||||
|
decreaseQty float64,
|
||||||
|
) error {
|
||||||
|
if stockLogRepo == nil || adjustmentID == 0 || productWarehouseID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if increaseQty == 0 && decreaseQty == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stockLogs, err := stockLogRepo.GetByProductWarehouse(ctx, productWarehouseID, 1)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get stock logs: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStock := 0.0
|
||||||
|
if len(stockLogs) > 0 {
|
||||||
|
currentStock = stockLogs[0].Stock
|
||||||
|
}
|
||||||
|
|
||||||
|
newLog := &entity.StockLog{
|
||||||
|
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||||
|
LoggableId: adjustmentID,
|
||||||
|
Notes: note,
|
||||||
|
ProductWarehouseId: productWarehouseID,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: increaseQty,
|
||||||
|
Decrease: decreaseQty,
|
||||||
|
Stock: currentStock + increaseQty - decreaseQty,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stockLogRepo.CreateOne(ctx, newLog, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
|
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
|
||||||
if err := s.Validate.Struct(query); err != nil {
|
if err := s.Validate.Struct(query); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
@@ -605,59 +605,11 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return errors.New("transaction is required")
|
return errors.New("transaction is required")
|
||||||
}
|
}
|
||||||
if s.FifoStockV2Svc == nil {
|
|
||||||
return errors.New("fifo v2 service is not available")
|
|
||||||
}
|
|
||||||
if desiredQty < 0 {
|
if desiredQty < 0 {
|
||||||
return errors.New("desired quantity must be zero or greater")
|
return errors.New("desired quantity must be zero or greater")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0); err != nil {
|
return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
asOf := chickin.ChickInDate
|
|
||||||
if asOf.IsZero() {
|
|
||||||
asOf = chickin.CreatedAt
|
|
||||||
}
|
|
||||||
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var refreshed entity.ProjectChickin
|
|
||||||
if err := tx.WithContext(ctx).
|
|
||||||
Where("id = ?", chickin.Id).
|
|
||||||
Take(&refreshed).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if refreshed.UsageQty > 0 {
|
|
||||||
decreaseLog := &entity.StockLog{
|
|
||||||
Decrease: refreshed.UsageQty,
|
|
||||||
LoggableType: string(utils.StockLogTypeChikin),
|
|
||||||
LoggableId: refreshed.Id,
|
|
||||||
ProductWarehouseId: refreshed.ProductWarehouseId,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
Notes: fmt.Sprintf("Chickin #%d", refreshed.Id),
|
|
||||||
}
|
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
|
||||||
}
|
|
||||||
if len(stockLogs) > 0 {
|
|
||||||
latestStockLog := stockLogs[0]
|
|
||||||
decreaseLog.Stock = latestStockLog.Stock
|
|
||||||
decreaseLog.Stock -= decreaseLog.Decrease
|
|
||||||
} else {
|
|
||||||
decreaseLog.Stock -= decreaseLog.Decrease
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
|
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
|
||||||
@@ -667,9 +619,6 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return errors.New("transaction is required")
|
return errors.New("transaction is required")
|
||||||
}
|
}
|
||||||
if s.FifoStockV2Svc == nil {
|
|
||||||
return errors.New("fifo v2 service is not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.WithContext(ctx).
|
if err := tx.WithContext(ctx).
|
||||||
Model(&entity.ProjectFlockPopulation{}).
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
@@ -678,11 +627,7 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
asOf := chickin.ChickInDate
|
return nil
|
||||||
if asOf.IsZero() {
|
|
||||||
asOf = chickin.CreatedAt
|
|
||||||
}
|
|
||||||
return reflowChickinScope(ctx, s.FifoStockV2Svc, tx, targetPW.Id, &asOf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
||||||
@@ -692,53 +637,11 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return errors.New("transaction is required")
|
return errors.New("transaction is required")
|
||||||
}
|
}
|
||||||
if s.FifoStockV2Svc == nil {
|
|
||||||
return errors.New("fifo v2 service is not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentUsage float64
|
|
||||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
asOf := chickin.ChickInDate
|
|
||||||
if asOf.IsZero() {
|
|
||||||
asOf = chickin.CreatedAt
|
|
||||||
}
|
|
||||||
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentUsage > 0 {
|
|
||||||
increaseLog := &entity.StockLog{
|
|
||||||
Increase: currentUsage,
|
|
||||||
LoggableType: string(utils.StockLogTypeChikin),
|
|
||||||
LoggableId: chickin.Id,
|
|
||||||
ProductWarehouseId: chickin.ProductWarehouseId,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
|
||||||
}
|
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, chickin.ProductWarehouseId, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
|
||||||
}
|
|
||||||
if len(stockLogs) > 0 {
|
|
||||||
latestStockLog := stockLogs[0]
|
|
||||||
increaseLog.Stock = latestStockLog.Stock
|
|
||||||
increaseLog.Stock += increaseLog.Increase
|
|
||||||
} else {
|
|
||||||
increaseLog.Stock += increaseLog.Increase
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,20 +25,27 @@ type RecordingRepository interface {
|
|||||||
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
||||||
ListByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, from *time.Time) ([]entity.Recording, error)
|
ListByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, from *time.Time) ([]entity.Recording, error)
|
||||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||||
|
CreateRecording(tx *gorm.DB, recording *entity.Recording) error
|
||||||
|
|
||||||
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
||||||
|
CreateStock(tx *gorm.DB, stock *entity.RecordingStock) error
|
||||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||||
|
DeleteStocksByIDs(tx *gorm.DB, ids []uint) error
|
||||||
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
||||||
|
GetStockByID(tx *gorm.DB, stockID uint) (*entity.RecordingStock, error)
|
||||||
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
|
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
|
||||||
UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error
|
UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error
|
||||||
|
UpdateDepletionQuantities(tx *gorm.DB, depletionID uint, qty, usageQty, pendingQty float64) error
|
||||||
|
|
||||||
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
||||||
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||||
ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error)
|
ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error)
|
||||||
|
GetDepletionByID(tx *gorm.DB, depletionID uint) (*entity.RecordingDepletion, error)
|
||||||
|
|
||||||
CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error
|
CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error
|
||||||
DeleteEggs(tx *gorm.DB, recordingID uint) error
|
DeleteEggs(tx *gorm.DB, recordingID uint) error
|
||||||
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
|
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
|
||||||
|
UpdateEggTotalQty(tx *gorm.DB, eggID uint, totalQty float64) error
|
||||||
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
|
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
|
||||||
|
|
||||||
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
||||||
@@ -272,6 +279,18 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda
|
|||||||
return nextRecordingDay(days), nil
|
return nextRecordingDay(days), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) CreateRecording(tx *gorm.DB, recording *entity.Recording) error {
|
||||||
|
if recording == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Select(
|
||||||
|
"ProjectFlockKandangId",
|
||||||
|
"RecordDatetime",
|
||||||
|
"Day",
|
||||||
|
"CreatedBy",
|
||||||
|
).Create(recording).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
|
||||||
if len(stocks) == 0 {
|
if len(stocks) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -279,10 +298,24 @@ func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.Reco
|
|||||||
return tx.Create(&stocks).Error
|
return tx.Create(&stocks).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) CreateStock(tx *gorm.DB, stock *entity.RecordingStock) error {
|
||||||
|
if stock == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Create(stock).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) error {
|
func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) error {
|
||||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
|
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) DeleteStocksByIDs(tx *gorm.DB, ids []uint) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) {
|
func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) {
|
||||||
var items []entity.RecordingStock
|
var items []entity.RecordingStock
|
||||||
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
||||||
@@ -291,6 +324,18 @@ func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]e
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetStockByID(tx *gorm.DB, stockID uint) (*entity.RecordingStock, error) {
|
||||||
|
if stockID == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var stock entity.RecordingStock
|
||||||
|
if err := tx.Where("id = ?", stockID).Take(&stock).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &stock, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error {
|
func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error {
|
||||||
return tx.Model(&entity.RecordingStock{}).
|
return tx.Model(&entity.RecordingStock{}).
|
||||||
Where("id = ?", stockID).
|
Where("id = ?", stockID).
|
||||||
@@ -306,6 +351,16 @@ func (r *RecordingRepositoryImpl) UpdateDepletionPending(tx *gorm.DB, depletionI
|
|||||||
Update("pending_qty", pendingQty).Error
|
Update("pending_qty", pendingQty).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) UpdateDepletionQuantities(tx *gorm.DB, depletionID uint, qty, usageQty, pendingQty float64) error {
|
||||||
|
return tx.Model(&entity.RecordingDepletion{}).
|
||||||
|
Where("id = ?", depletionID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"qty": qty,
|
||||||
|
"usage_qty": usageQty,
|
||||||
|
"pending_qty": pendingQty,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
||||||
if len(depletions) == 0 {
|
if len(depletions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -325,6 +380,18 @@ func (r *RecordingRepositoryImpl) ListDepletions(tx *gorm.DB, recordingID uint)
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetDepletionByID(tx *gorm.DB, depletionID uint) (*entity.RecordingDepletion, error) {
|
||||||
|
if depletionID == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var depletion entity.RecordingDepletion
|
||||||
|
if err := tx.Where("id = ?", depletionID).Take(&depletion).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &depletion, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error {
|
func (r *RecordingRepositoryImpl) CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error {
|
||||||
if len(eggs) == 0 {
|
if len(eggs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -344,6 +411,12 @@ func (r *RecordingRepositoryImpl) ListEggs(tx *gorm.DB, recordingID uint) ([]ent
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) UpdateEggTotalQty(tx *gorm.DB, eggID uint, totalQty float64) error {
|
||||||
|
return tx.Model(&entity.RecordingEgg{}).
|
||||||
|
Where("id = ?", eggID).
|
||||||
|
Update("total_qty", totalQty).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id uint,
|
id uint,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -233,7 +233,7 @@ var adjustmentSubtypesByType = map[AdjustmentTransactionType][]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hiddenAdjustmentSubtypesForFrontend = map[string]struct{}{
|
var hiddenAdjustmentSubtypesForFrontend = map[string]struct{}{
|
||||||
string(AdjustmentTransactionSubtypeRecordingDepletionIn): {},
|
string(AdjustmentTransactionSubtypeRecordingDepletionOut): {},
|
||||||
}
|
}
|
||||||
|
|
||||||
var adjustmentSubtypeToType = func() map[string]AdjustmentTransactionType {
|
var adjustmentSubtypeToType = func() map[string]AdjustmentTransactionType {
|
||||||
|
|||||||
Reference in New Issue
Block a user