mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-21 05:45:44 +00:00
Merge branch 'development' into 'production'
Development See merge request mbugroup/lti-api!505
This commit is contained in:
+18
-4
@@ -27,10 +27,24 @@ workflow:
|
||||
.ecr_login: &ecr_login |
|
||||
AWS_CLI_ENV_ARGS=""
|
||||
AWS_CLI_ENV_ARGS="$AWS_CLI_ENV_ARGS -e AWS_REGION=$AWS_REGION"
|
||||
AWS_CLI_ENV_ARGS="$AWS_CLI_ENV_ARGS -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}"
|
||||
AWS_CLI_ENV_ARGS="$AWS_CLI_ENV_ARGS -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}"
|
||||
if [ -n "${AWS_SESSION_TOKEN:-}" ]; then
|
||||
AWS_CLI_ENV_ARGS="$AWS_CLI_ENV_ARGS -e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN"
|
||||
|
||||
HAS_ACCESS_KEY="false"
|
||||
HAS_SECRET_KEY="false"
|
||||
if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
|
||||
HAS_ACCESS_KEY="true"
|
||||
fi
|
||||
if [ -n "${AWS_SECRET_ACCESS_KEY:-}" ]; then
|
||||
HAS_SECRET_KEY="true"
|
||||
fi
|
||||
|
||||
if [ "$HAS_ACCESS_KEY" = "true" ] && [ "$HAS_SECRET_KEY" = "true" ]; then
|
||||
AWS_CLI_ENV_ARGS="$AWS_CLI_ENV_ARGS -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID"
|
||||
AWS_CLI_ENV_ARGS="$AWS_CLI_ENV_ARGS -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY"
|
||||
if [ -n "${AWS_SESSION_TOKEN:-}" ]; then
|
||||
AWS_CLI_ENV_ARGS="$AWS_CLI_ENV_ARGS -e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN"
|
||||
fi
|
||||
elif [ "$HAS_ACCESS_KEY" = "true" ] || [ "$HAS_SECRET_KEY" = "true" ] || [ -n "${AWS_SESSION_TOKEN:-}" ]; then
|
||||
echo "WARN: Incomplete AWS_* env vars detected; ignoring injected AWS credentials for ECR login."
|
||||
fi
|
||||
|
||||
PASS="$(docker run --rm $AWS_CLI_ENV_ARGS public.ecr.aws/aws-cli/aws-cli:latest \
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
// Command: fix-stock-log-drift
|
||||
//
|
||||
// Tujuan:
|
||||
// Sinkronkan `stock_logs.stock` (running ledger) dengan `product_warehouses.qty`
|
||||
// (FIFO truth) ketika keduanya drift.
|
||||
//
|
||||
// Drift biasanya terjadi karena bug di Recording-Edit (sebelum fix) yang
|
||||
// hanya menulis -decrease tanpa +increase saat in-place update. Akibatnya
|
||||
// running ledger di stock_logs tertinggal dari qty riil di product_warehouses.
|
||||
//
|
||||
// Cara kerja:
|
||||
// 1. Ambil product_warehouses.qty (sebagai truth)
|
||||
// 2. Ambil last_stock_log.stock
|
||||
// 3. Cari recording yang berkontribusi pada drift (untuk notes)
|
||||
// 4. Hitung drift = qty - last_stock_log.stock
|
||||
// 5. Jika drift != 0, insert 1 stock_log corrective:
|
||||
// - drift > 0 → increase = drift
|
||||
// - drift < 0 → decrease = |drift|
|
||||
// stock akhir akan sama dengan qty (truth).
|
||||
// Notes otomatis berisi daftar recording IDs yang berkontribusi pada drift.
|
||||
//
|
||||
// Mode:
|
||||
// --apply=false (default) → dry-run, hanya tampilkan rencana
|
||||
// --apply=true → eksekusi insert
|
||||
//
|
||||
// Contoh:
|
||||
// go run cmd/fix-stock-log-drift/main.go --product-warehouse-id=1261
|
||||
// go run cmd/fix-stock-log-drift/main.go --product-warehouse-id=1261 --apply
|
||||
// go run cmd/fix-stock-log-drift/main.go --product-warehouse-id=1261 --apply \
|
||||
// --actor-id=1 --notes="Koreksi manual drift"
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
qtyEpsilon = 1e-6
|
||||
defaultActorID uint = 1
|
||||
maxSuspectInNotes = 30
|
||||
)
|
||||
|
||||
type driftRow struct {
|
||||
ProductWarehouseID uint `gorm:"column:product_warehouse_id"`
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
WarehouseName string `gorm:"column:warehouse_name"`
|
||||
CurrentQty float64 `gorm:"column:current_qty"`
|
||||
LastLogStock float64 `gorm:"column:last_log_stock"`
|
||||
LastLogID uint `gorm:"column:last_log_id"`
|
||||
FifoExpected float64 `gorm:"column:fifo_expected"`
|
||||
}
|
||||
|
||||
type suspectRecording struct {
|
||||
RecordingID uint `gorm:"column:recording_id"`
|
||||
FifoUsage float64 `gorm:"column:fifo_usage"`
|
||||
NetLogConsumed float64 `gorm:"column:net_log_consumed"`
|
||||
Phantom float64 `gorm:"column:phantom"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
productWarehouseID uint
|
||||
apply bool
|
||||
actorID uint
|
||||
notes string
|
||||
)
|
||||
|
||||
flag.UintVar(&productWarehouseID, "product-warehouse-id", 0, "Target product_warehouse_id (required)")
|
||||
flag.BoolVar(&apply, "apply", false, "Apply changes. If false, run as dry-run")
|
||||
flag.UintVar(&actorID, "actor-id", defaultActorID, "User id yang akan dicatat sebagai created_by stock_log corrective")
|
||||
flag.StringVar(¬es, "notes", "", "Custom notes untuk stock_log corrective (opsional, default=auto-generate dari data recording)")
|
||||
flag.Parse()
|
||||
|
||||
notes = strings.TrimSpace(notes)
|
||||
if err := validateFlags(productWarehouseID, actorID); err != nil {
|
||||
log.Fatalf("invalid flags: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
db := database.Connect(config.DBHost, config.DBName)
|
||||
|
||||
row, err := loadDriftRow(ctx, db, productWarehouseID)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load product warehouse: %v", err)
|
||||
}
|
||||
|
||||
suspects, err := loadSuspectRecordings(ctx, db, productWarehouseID)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load suspect recordings: %v", err)
|
||||
}
|
||||
|
||||
drift := row.CurrentQty - row.LastLogStock
|
||||
|
||||
// Print info header
|
||||
fmt.Printf("Mode: %s\n", modeLabel(apply))
|
||||
fmt.Printf("Target product_warehouse_id: %d\n", productWarehouseID)
|
||||
fmt.Printf("Product: %q\n", row.ProductName)
|
||||
fmt.Printf("Warehouse: %q\n", row.WarehouseName)
|
||||
fmt.Printf("Current qty (product_warehouses): %.3f\n", row.CurrentQty)
|
||||
fmt.Printf("FIFO expected (sum total_qty - total_used): %.3f\n", row.FifoExpected)
|
||||
fmt.Printf("Last stock_log: id=%d stock=%.3f\n", row.LastLogID, row.LastLogStock)
|
||||
fmt.Printf("Drift (qty - last_log_stock): %+.3f\n", drift)
|
||||
|
||||
if !nearlyEqual(row.CurrentQty, row.FifoExpected) {
|
||||
fmt.Println()
|
||||
fmt.Println("⚠️ WARNING: product_warehouses.qty TIDAK match dengan FIFO expected.")
|
||||
fmt.Println(" Disarankan jalankan dulu cmd/reflow-quantity-product-warehouse-from-stock-allocation")
|
||||
fmt.Println(" sebelum fix stock_log drift, agar truth source-nya sudah benar.")
|
||||
}
|
||||
|
||||
// Print suspect recordings
|
||||
fmt.Println()
|
||||
if len(suspects) > 0 {
|
||||
totalPhantom := 0.0
|
||||
for _, s := range suspects {
|
||||
totalPhantom += s.Phantom
|
||||
}
|
||||
fmt.Printf("Suspect recordings (drift contributors): %d\n", len(suspects))
|
||||
for _, s := range suspects {
|
||||
fmt.Printf(
|
||||
" #%-6d fifo=%-10.3f net_log=%-10.3f phantom=%+.3f\n",
|
||||
s.RecordingID, s.FifoUsage, s.NetLogConsumed, s.Phantom,
|
||||
)
|
||||
}
|
||||
fmt.Printf("Total suspect phantom: %+.3f\n", totalPhantom)
|
||||
} else {
|
||||
fmt.Println("Suspect recordings: none found (drift origin unknown)")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if nearlyEqual(drift, 0) {
|
||||
fmt.Println("✓ Tidak ada drift. Stock_log sudah sinkron dengan product_warehouses.qty.")
|
||||
fmt.Println("Summary: planned=0 inserted=0 skipped=1 failed=0")
|
||||
return
|
||||
}
|
||||
|
||||
// Build notes if not provided
|
||||
if notes == "" {
|
||||
notes = buildDefaultNotes(row, drift, suspects)
|
||||
}
|
||||
|
||||
plan := buildCorrectiveLog(row, drift, actorID, notes)
|
||||
|
||||
fmt.Printf(
|
||||
"PLAN insert stock_log:\n pw=%d increase=%.3f decrease=%.3f stock=%.3f\n notes=%q\n",
|
||||
plan.ProductWarehouseId,
|
||||
plan.Increase,
|
||||
plan.Decrease,
|
||||
plan.Stock,
|
||||
plan.Notes,
|
||||
)
|
||||
|
||||
if !apply {
|
||||
fmt.Println()
|
||||
fmt.Println("Summary: planned=1 inserted=0 skipped=0 failed=0 (dry-run)")
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// Re-check di dalam transaction agar aman dari race condition
|
||||
current, err := loadDriftRow(ctx, tx, productWarehouseID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("re-read product_warehouse_id=%d: %w", productWarehouseID, err)
|
||||
}
|
||||
currentDrift := current.CurrentQty - current.LastLogStock
|
||||
if nearlyEqual(currentDrift, 0) {
|
||||
fmt.Println("Drift hilang sebelum insert (kemungkinan ada operasi paralel). Skip.")
|
||||
return nil
|
||||
}
|
||||
fresh := buildCorrectiveLog(current, currentDrift, actorID, notes)
|
||||
if err := tx.Create(&fresh).Error; err != nil {
|
||||
return fmt.Errorf("insert corrective stock_log for pw=%d: %w", productWarehouseID, err)
|
||||
}
|
||||
fmt.Printf(
|
||||
"DONE inserted stock_log id=%d pw=%d increase=%.3f decrease=%.3f stock=%.3f\n",
|
||||
fresh.Id,
|
||||
fresh.ProductWarehouseId,
|
||||
fresh.Increase,
|
||||
fresh.Decrease,
|
||||
fresh.Stock,
|
||||
)
|
||||
return nil
|
||||
}); err != nil {
|
||||
fmt.Println()
|
||||
fmt.Println("Summary: planned=1 inserted=0 skipped=0 failed=1")
|
||||
log.Printf("error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Summary: planned=1 inserted=1 skipped=0 failed=0")
|
||||
}
|
||||
|
||||
func validateFlags(productWarehouseID uint, actorID uint) error {
|
||||
if productWarehouseID == 0 {
|
||||
return errors.New("--product-warehouse-id is required (must be > 0)")
|
||||
}
|
||||
if actorID == 0 {
|
||||
return errors.New("--actor-id must be > 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDriftRow(ctx context.Context, db *gorm.DB, productWarehouseID uint) (driftRow, error) {
|
||||
row := driftRow{}
|
||||
|
||||
lastLogSub := db.WithContext(ctx).
|
||||
Table("stock_logs").
|
||||
Select("id, product_warehouse_id, stock").
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Order("id DESC").
|
||||
Limit(1)
|
||||
|
||||
fifoSub := db.WithContext(ctx).
|
||||
Table("purchase_items").
|
||||
Select(`
|
||||
product_warehouse_id,
|
||||
COALESCE(SUM(COALESCE(total_qty, 0) - COALESCE(total_used, 0)), 0) AS fifo_expected
|
||||
`).
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Group("product_warehouse_id")
|
||||
|
||||
if err := db.WithContext(ctx).
|
||||
Table("product_warehouses pw").
|
||||
Select(`
|
||||
pw.id AS product_warehouse_id,
|
||||
pw.product_id AS product_id,
|
||||
COALESCE(p.name, '') AS product_name,
|
||||
COALESCE(w.name, '') AS warehouse_name,
|
||||
COALESCE(pw.qty, 0) AS current_qty,
|
||||
COALESCE(last_log.stock, 0) AS last_log_stock,
|
||||
COALESCE(last_log.id, 0) AS last_log_id,
|
||||
COALESCE(fifo.fifo_expected, 0) AS fifo_expected
|
||||
`).
|
||||
Joins("LEFT JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("LEFT JOIN (?) last_log ON last_log.product_warehouse_id = pw.id", lastLogSub).
|
||||
Joins("LEFT JOIN (?) fifo ON fifo.product_warehouse_id = pw.id", fifoSub).
|
||||
Where("pw.id = ?", productWarehouseID).
|
||||
Scan(&row).Error; err != nil {
|
||||
return row, err
|
||||
}
|
||||
|
||||
if row.ProductWarehouseID == 0 {
|
||||
return row, fmt.Errorf("product_warehouse_id=%d not found", productWarehouseID)
|
||||
}
|
||||
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// loadSuspectRecordings mencari recording yang net stock_log consumed-nya
|
||||
// melebihi FIFO usage_qty — ini adalah recording yang berkontribusi pada drift
|
||||
// akibat bug Recording-Edit yang hanya menulis -decrease tanpa +increase.
|
||||
func loadSuspectRecordings(ctx context.Context, db *gorm.DB, productWarehouseID uint) ([]suspectRecording, error) {
|
||||
rows := make([]suspectRecording, 0)
|
||||
if err := db.WithContext(ctx).
|
||||
Table("recording_stocks rs").
|
||||
Select(`
|
||||
rs.recording_id,
|
||||
COALESCE(rs.usage_qty, 0) AS fifo_usage,
|
||||
(
|
||||
COALESCE(SUM(CASE WHEN sl.decrease > 0 THEN sl.decrease ELSE 0 END), 0) -
|
||||
COALESCE(SUM(CASE WHEN sl.increase > 0 THEN sl.increase ELSE 0 END), 0)
|
||||
) AS net_log_consumed,
|
||||
(
|
||||
COALESCE(SUM(CASE WHEN sl.decrease > 0 THEN sl.decrease ELSE 0 END), 0) -
|
||||
COALESCE(SUM(CASE WHEN sl.increase > 0 THEN sl.increase ELSE 0 END), 0) -
|
||||
COALESCE(rs.usage_qty, 0)
|
||||
) AS phantom
|
||||
`).
|
||||
Joins(`
|
||||
JOIN stock_logs sl ON sl.loggable_type = ?
|
||||
AND sl.loggable_id = rs.recording_id
|
||||
AND sl.product_warehouse_id = rs.product_warehouse_id
|
||||
`, string(utils.StockLogTypeRecording)).
|
||||
Where("rs.product_warehouse_id = ?", productWarehouseID).
|
||||
Group("rs.recording_id, rs.usage_qty").
|
||||
Having(`
|
||||
ABS(
|
||||
COALESCE(SUM(CASE WHEN sl.decrease > 0 THEN sl.decrease ELSE 0 END), 0) -
|
||||
COALESCE(SUM(CASE WHEN sl.increase > 0 THEN sl.increase ELSE 0 END), 0) -
|
||||
COALESCE(rs.usage_qty, 0)
|
||||
) > ?
|
||||
`, qtyEpsilon).
|
||||
Order("rs.recording_id ASC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// buildDefaultNotes membuat notes otomatis yang berisi penjelasan drift
|
||||
// beserta daftar recording_id yang berkontribusi + phantom amount masing-masing.
|
||||
func buildDefaultNotes(row driftRow, drift float64, suspects []suspectRecording) string {
|
||||
sign := "+"
|
||||
if drift < 0 {
|
||||
sign = ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf(
|
||||
"Koreksi drift stock_log akibat bug Recording-Edit (in-place update menulis -decrease tanpa +increase). PW=%d (%s) drift=%s%.3f.",
|
||||
row.ProductWarehouseID,
|
||||
row.WarehouseName,
|
||||
sign,
|
||||
drift,
|
||||
))
|
||||
|
||||
if len(suspects) == 0 {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
sb.WriteString(" Recordings affected:")
|
||||
|
||||
limit := len(suspects)
|
||||
truncated := 0
|
||||
if limit > maxSuspectInNotes {
|
||||
truncated = limit - maxSuspectInNotes
|
||||
limit = maxSuspectInNotes
|
||||
}
|
||||
|
||||
for i := 0; i < limit; i++ {
|
||||
s := suspects[i]
|
||||
phantomSign := "+"
|
||||
if s.Phantom < 0 {
|
||||
phantomSign = ""
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" #%d(%s%.0f)", s.RecordingID, phantomSign, s.Phantom))
|
||||
if i < limit-1 || truncated > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
}
|
||||
|
||||
if truncated > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" ... (+%d more)", truncated))
|
||||
}
|
||||
|
||||
sb.WriteString(".")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func buildCorrectiveLog(row driftRow, drift float64, actorID uint, notes string) entity.StockLog {
|
||||
corrective := entity.StockLog{
|
||||
ProductWarehouseId: row.ProductWarehouseID,
|
||||
CreatedBy: actorID,
|
||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||
LoggableId: 0,
|
||||
Stock: row.CurrentQty,
|
||||
Notes: notes,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if drift > 0 {
|
||||
corrective.Increase = drift
|
||||
corrective.Decrease = 0
|
||||
} else {
|
||||
corrective.Increase = 0
|
||||
corrective.Decrease = -drift
|
||||
}
|
||||
return corrective
|
||||
}
|
||||
|
||||
func modeLabel(apply bool) string {
|
||||
if apply {
|
||||
return "APPLY"
|
||||
}
|
||||
return "DRY-RUN"
|
||||
}
|
||||
|
||||
func nearlyEqual(a, b float64) bool {
|
||||
return math.Abs(a-b) <= qtyEpsilon
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func FlockAgeDay(originDate time.Time, periodDate time.Time) int {
|
||||
if period.Before(origin) {
|
||||
return 0
|
||||
}
|
||||
return int(period.Sub(origin).Hours()/24) + 1
|
||||
return int(period.Sub(origin).Hours() / 24)
|
||||
}
|
||||
|
||||
func DepreciationScheduleDay(originDate time.Time, periodDate time.Time, houseType string) int {
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS idx_daily_checklists_unique_non_rejected;
|
||||
|
||||
ALTER TABLE daily_checklists
|
||||
ADD CONSTRAINT daily_checklists_date_kandang_category_key
|
||||
UNIQUE (date, kandang_id, category);
|
||||
|
||||
COMMIT;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE daily_checklists
|
||||
DROP CONSTRAINT IF EXISTS daily_checklists_date_kandang_category_key;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_daily_checklists_unique_non_rejected
|
||||
ON daily_checklists (date, kandang_id, category)
|
||||
WHERE (status IS NULL OR status <> 'REJECTED');
|
||||
|
||||
COMMIT;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
UPDATE recordings r
|
||||
SET day = (
|
||||
SELECT (r.record_datetime::date - MIN(pc.chick_in_date)::date)::int + 1
|
||||
FROM project_chickins pc
|
||||
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
|
||||
AND pc.deleted_at IS NULL
|
||||
)
|
||||
WHERE r.deleted_at IS NULL
|
||||
AND (
|
||||
SELECT MIN(pc.chick_in_date)
|
||||
FROM project_chickins pc
|
||||
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
|
||||
AND pc.deleted_at IS NULL
|
||||
) IS NOT NULL;
|
||||
|
||||
ALTER TABLE recordings
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_day;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_day
|
||||
CHECK (day IS NULL OR day >= 1);
|
||||
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE recordings
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_day;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_day
|
||||
CHECK (day IS NULL OR day >= 0);
|
||||
|
||||
UPDATE recordings r
|
||||
SET day = (
|
||||
SELECT (r.record_datetime::date - MIN(pc.chick_in_date)::date)::int
|
||||
FROM project_chickins pc
|
||||
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
|
||||
AND pc.deleted_at IS NULL
|
||||
)
|
||||
WHERE r.deleted_at IS NULL
|
||||
AND (
|
||||
SELECT MIN(pc.chick_in_date)
|
||||
FROM project_chickins pc
|
||||
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
|
||||
AND pc.deleted_at IS NULL
|
||||
) IS NOT NULL;
|
||||
@@ -277,7 +277,11 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
||||
normalizedSearch := re.ReplaceAllString(params.Search, "")
|
||||
if normalizedSearch != "" {
|
||||
like := "%" + normalizedSearch + "%"
|
||||
db = db.Where("(regexp_replace(k.name, '[^a-zA-Z0-9]', '', 'g') ILIKE ? OR regexp_replace(dc.category::text, '[^a-zA-Z0-9]', '', 'g') ILIKE ?)", like, like)
|
||||
db = db.Where(`(
|
||||
regexp_replace(k.name, '[^a-zA-Z0-9]', '', 'g') ILIKE ? OR
|
||||
regexp_replace(dc.category::text, '[^a-zA-Z0-9]', '', 'g') ILIKE ? OR
|
||||
(dc.category = 'empty_kandang' AND regexp_replace('Kandang Kosong', '[^a-zA-Z0-9]', '', 'g') ILIKE ?)
|
||||
)`, like, like, like)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,9 +119,9 @@ func (r *DashboardRepositoryImpl) GetRecordingWeeklyMetrics(ctx context.Context,
|
||||
var rows []RecordingWeeklyMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
WHEN r.day IS NULL OR r.day < 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN (r.day / 7 + 1) + 17
|
||||
ELSE (r.day / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
@@ -503,9 +503,9 @@ func (r *DashboardRepositoryImpl) GetComparisonWeeklyMetrics(ctx context.Context
|
||||
|
||||
var rows []ComparisonWeeklyMetric
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
WHEN r.day IS NULL OR r.day < 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN (r.day / 7 + 1) + 17
|
||||
ELSE (r.day / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
@@ -574,9 +574,9 @@ func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context
|
||||
var rows []EggQualityWeeklyMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
WHEN r.day IS NULL OR r.day < 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN (r.day / 7 + 1) + 17
|
||||
ELSE (r.day / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
@@ -616,9 +616,9 @@ func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, s
|
||||
var rows []WeeklyEggWeightMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
WHEN r.day IS NULL OR r.day < 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN (r.day / 7 + 1) + 17
|
||||
ELSE (r.day / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
@@ -647,9 +647,9 @@ func (r *DashboardRepositoryImpl) GetFeedUsageWeeklyByUom(ctx context.Context, s
|
||||
var rows []WeeklyFeedUsageMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
WHEN r.day IS NULL OR r.day < 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN (r.day / 7 + 1) + 17
|
||||
ELSE (r.day / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
|
||||
@@ -24,9 +24,11 @@ func NewEmployeesController(employeesService service.EmployeesService) *Employee
|
||||
|
||||
func (u *EmployeesController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
OrderBy: c.Query("order_by", "desc"),
|
||||
SortBy: c.Query("sort_by", "updated_at"),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -126,11 +127,18 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
if params.IsActive != nil {
|
||||
db = db.Where("employees.is_active = ?", *params.IsActive)
|
||||
}
|
||||
return db.
|
||||
|
||||
db = db.
|
||||
Select("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at").
|
||||
Group("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at").
|
||||
Order("employees.created_at DESC").
|
||||
Order("employees.updated_at DESC")
|
||||
Group("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at")
|
||||
|
||||
if params.OrderBy == "desc" || params.OrderBy == "" {
|
||||
db = db.Order(fmt.Sprintf("employees.%s DESC", params.SortBy))
|
||||
} else {
|
||||
db = db.Order(fmt.Sprintf("employees.%s ASC", params.SortBy))
|
||||
}
|
||||
|
||||
return db
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -18,4 +18,6 @@ type Query struct {
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
KandangId *uint `query:"kandang_id" validate:"omitempty"`
|
||||
IsActive *bool `query:"is_active" validate:"omitempty"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"updated_at"`
|
||||
OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"desc"`
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ func (u *KandangGroupController) GetAll(c *fiber.Ctx) error {
|
||||
Search: c.Query("search", ""),
|
||||
LocationId: c.QueryInt("location_id", 0),
|
||||
PicId: c.QueryInt("pic_id", 0),
|
||||
OrderBy: c.Query("order_by", "desc"),
|
||||
SortBy: c.Query("sort_by", "updated_at"),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
|
||||
@@ -70,7 +70,14 @@ func (s kandangGroupService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
||||
if params.PicId != 0 {
|
||||
db = db.Where("kandang_groups.pic_id = ?", params.PicId)
|
||||
}
|
||||
return db.Order("kandang_groups.created_at DESC").Order("kandang_groups.updated_at DESC")
|
||||
|
||||
if params.OrderBy == "desc" || params.OrderBy == "" {
|
||||
db = db.Order(fmt.Sprintf("kandang_groups.%s DESC", params.SortBy))
|
||||
} else {
|
||||
db = db.Order(fmt.Sprintf("kandang_groups.%s ASC", params.SortBy))
|
||||
}
|
||||
|
||||
return db
|
||||
})
|
||||
|
||||
if scopeErr != nil {
|
||||
|
||||
@@ -20,4 +20,6 @@ type Query struct {
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"updated_at"`
|
||||
OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"desc"`
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ func (u *KandangController) GetAll(c *fiber.Ctx) error {
|
||||
Search: c.Query("search", ""),
|
||||
LocationId: c.QueryInt("location_id", 0),
|
||||
PicId: c.QueryInt("pic_id", 0),
|
||||
OrderBy: c.Query("order_by", "desc"),
|
||||
SortBy: c.Query("sort_by", "created_at"),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
|
||||
@@ -66,7 +66,14 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
||||
if params.PicId != 0 {
|
||||
db = db.Where("pic_id = ?", params.PicId)
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
|
||||
if params.OrderBy == "desc" || params.OrderBy == "" {
|
||||
db = db.Order(fmt.Sprintf("%s DESC", params.SortBy))
|
||||
} else {
|
||||
db = db.Order(fmt.Sprintf("%s ASC", params.SortBy))
|
||||
}
|
||||
|
||||
return db
|
||||
})
|
||||
|
||||
if scopeErr != nil {
|
||||
|
||||
@@ -26,4 +26,6 @@ type Query struct {
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"updated_at"`
|
||||
OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"desc"`
|
||||
}
|
||||
|
||||
@@ -2460,7 +2460,7 @@ func (s *recordingService) computeRecordingDay(ctx context.Context, projectFlock
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, "Record date tidak boleh sebelum tanggal chick in")
|
||||
}
|
||||
|
||||
return diff + 1, nil
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
|
||||
@@ -2621,8 +2621,8 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
|
||||
if isGrowing {
|
||||
week := 0
|
||||
if recording.Day != nil && *recording.Day > 0 {
|
||||
week = (*recording.Day-1)/7 + 1
|
||||
if recording.Day != nil && *recording.Day >= 0 {
|
||||
week = *recording.Day/7 + 1
|
||||
}
|
||||
if week > 0 && s.Repository != nil {
|
||||
meanBw, ok, err := s.Repository.GetUniformityMeanBwByWeek(tx, recording.ProjectFlockKandangId, week)
|
||||
@@ -3121,6 +3121,12 @@ func (s *recordingService) reflowSyncRecordingStocks(
|
||||
existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock)
|
||||
}
|
||||
|
||||
shouldWriteLog := shouldWriteRecordingStockLog(note, actorID)
|
||||
if shouldWriteLog && s.StockLogRepo == nil {
|
||||
return errors.New("stock log repository is not available")
|
||||
}
|
||||
resetLogState := newRecordingStockLogState()
|
||||
|
||||
stocksToApply := make([]entity.RecordingStock, 0, len(incoming))
|
||||
for _, item := range incoming {
|
||||
list := existingByWarehouse[item.ProductWarehouseId]
|
||||
@@ -3128,6 +3134,25 @@ func (s *recordingService) reflowSyncRecordingStocks(
|
||||
if len(list) > 0 {
|
||||
stock = list[0]
|
||||
existingByWarehouse[item.ProductWarehouseId] = list[1:]
|
||||
|
||||
// Write reset (increase) stock_log for the OLD consumption BEFORE overwriting UsageQty.
|
||||
// FIFO internally does Rollback+Reallocate inside reflowApplyRecordingStocks, but the
|
||||
// corresponding +increase stock_log for the rollback step was previously missing, causing
|
||||
// stock_log.stock to drift below the true FIFO qty on every in-place edit.
|
||||
rollbackQty := recordingStockRollbackQty(stock)
|
||||
if rollbackQty > 1e-6 && shouldWriteLog {
|
||||
resetLog := &entity.StockLog{
|
||||
ProductWarehouseId: stock.ProductWarehouseId,
|
||||
CreatedBy: actorID,
|
||||
Increase: rollbackQty,
|
||||
LoggableType: string(utils.StockLogTypeRecording),
|
||||
LoggableId: stock.RecordingId,
|
||||
Notes: note,
|
||||
}
|
||||
if err := s.appendRecordingStockLog(ctx, tx, resetLogState, resetLog); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
zero := 0.0
|
||||
stock = entity.RecordingStock{
|
||||
|
||||
@@ -3,13 +3,11 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/xuri/excelize/v2"
|
||||
@@ -45,7 +43,6 @@ func buildPurchaseExportWorkbook(purchases []entity.Purchase) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
listItems := dto.ToPurchaseListDTOs(purchases)
|
||||
grandTotals := buildPurchaseGrandTotalMap(purchases)
|
||||
|
||||
if err := setPurchaseExportColumns(file, purchaseExportSheetName); err != nil {
|
||||
@@ -54,7 +51,7 @@ func buildPurchaseExportWorkbook(purchases []entity.Purchase) ([]byte, error) {
|
||||
if err := setPurchaseExportHeaders(file, purchaseExportSheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setPurchaseExportRows(file, purchaseExportSheetName, listItems, grandTotals); err != nil {
|
||||
if err := setPurchaseExportRows(file, purchaseExportSheetName, purchases, grandTotals); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := file.SetPanes(purchaseExportSheetName, &excelize.Panes{
|
||||
@@ -81,10 +78,11 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error {
|
||||
"D": 14,
|
||||
"E": 22,
|
||||
"F": 22,
|
||||
"G": 18,
|
||||
"H": 18,
|
||||
"I": 52,
|
||||
"J": 24,
|
||||
"G": 22,
|
||||
"H": 32,
|
||||
"I": 18,
|
||||
"J": 18,
|
||||
"K": 24,
|
||||
}
|
||||
|
||||
for col, width := range columnWidths {
|
||||
@@ -107,9 +105,10 @@ func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
|
||||
"Tanggal Terima",
|
||||
"Supplier",
|
||||
"Lokasi",
|
||||
"Gudang",
|
||||
"Product",
|
||||
"Status",
|
||||
"Grand Total",
|
||||
"Products",
|
||||
"Notes",
|
||||
}
|
||||
|
||||
@@ -138,49 +137,34 @@ func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return file.SetCellStyle(sheet, "A1", "J1", headerStyle)
|
||||
return file.SetCellStyle(sheet, "A1", "K1", headerStyle)
|
||||
}
|
||||
|
||||
func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.PurchaseListDTO, grandTotals map[uint]float64) error {
|
||||
if len(items) == 0 {
|
||||
func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase, grandTotals map[uint]float64) error {
|
||||
if len(purchases) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, item := range items {
|
||||
row := strconv.Itoa(i + 2)
|
||||
if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(item.PrNumber)); err != nil {
|
||||
return err
|
||||
rowIdx := 2
|
||||
for p := range purchases {
|
||||
purchase := &purchases[p]
|
||||
total := grandTotals[purchase.Id]
|
||||
if len(purchase.Items) == 0 {
|
||||
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, total); err != nil {
|
||||
return err
|
||||
}
|
||||
rowIdx++
|
||||
continue
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "B"+row, safePurchaseExportPointerText(item.PoNumber)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "C"+row, formatPurchaseExportDate(item.PoDate)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "D"+row, formatPurchaseExportDate(item.ReceivedDate)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "E"+row, safePurchaseSupplierName(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "F"+row, safePurchaseLocationName(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "G"+row, formatPurchaseExportStatus(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "H"+row, formatPurchaseRupiah(grandTotals[item.Id])); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "I"+row, formatPurchaseProducts(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "J"+row, safePurchaseExportPointerText(item.Notes)); err != nil {
|
||||
return err
|
||||
for it := range purchase.Items {
|
||||
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], total); err != nil {
|
||||
return err
|
||||
}
|
||||
rowIdx++
|
||||
}
|
||||
}
|
||||
|
||||
lastRow := len(items) + 1
|
||||
lastRow := rowIdx - 1
|
||||
dataStyle, err := file.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "left",
|
||||
@@ -197,7 +181,7 @@ func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.Purcha
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellStyle(sheet, "A2", "J"+strconv.Itoa(lastRow), dataStyle); err != nil {
|
||||
if err := file.SetCellStyle(sheet, "A2", "K"+strconv.Itoa(lastRow), dataStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -217,7 +201,59 @@ func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.Purcha
|
||||
return err
|
||||
}
|
||||
|
||||
return file.SetCellStyle(sheet, "H2", "H"+strconv.Itoa(lastRow), moneyStyle)
|
||||
return file.SetCellStyle(sheet, "J2", "J"+strconv.Itoa(lastRow), moneyStyle)
|
||||
}
|
||||
|
||||
func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, grandTotal float64) error {
|
||||
row := strconv.Itoa(rowIdx)
|
||||
|
||||
// Purchase-level columns (repeat across rows of the same purchase)
|
||||
if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(purchase.PrNumber)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "B"+row, safePurchaseExportPointerText(purchase.PoNumber)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "C"+row, formatPurchaseExportDate(purchase.PoDate)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "E"+row, safePurchaseExportEntitySupplierName(purchase)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "I"+row, formatPurchaseExportEntityStatus(purchase)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "J"+row, formatPurchaseRupiah(grandTotal)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "K"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Item-level columns
|
||||
if item == nil {
|
||||
for _, col := range []string{"D", "F", "G", "H"} {
|
||||
if err := file.SetCellValue(sheet, col+row, "-"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := file.SetCellValue(sheet, "D"+row, formatPurchaseExportDate(item.ReceivedDate)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "F"+row, safePurchaseItemLocationName(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "G"+row, safePurchaseWarehouseName(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "H"+row, safePurchaseItemProductName(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildPurchaseGrandTotalMap(items []entity.Purchase) map[uint]float64 {
|
||||
@@ -232,31 +268,45 @@ func buildPurchaseGrandTotalMap(items []entity.Purchase) map[uint]float64 {
|
||||
return result
|
||||
}
|
||||
|
||||
func safePurchaseSupplierName(item dto.PurchaseListDTO) string {
|
||||
if item.Supplier == nil {
|
||||
func safePurchaseExportEntitySupplierName(purchase *entity.Purchase) string {
|
||||
if purchase.Supplier.Id == 0 {
|
||||
return "-"
|
||||
}
|
||||
return safePurchaseExportText(item.Supplier.Name)
|
||||
return safePurchaseExportText(purchase.Supplier.Name)
|
||||
}
|
||||
|
||||
func safePurchaseLocationName(item dto.PurchaseListDTO) string {
|
||||
if item.Location == nil {
|
||||
func safePurchaseWarehouseName(item *entity.PurchaseItem) string {
|
||||
if item.Warehouse == nil {
|
||||
return "-"
|
||||
}
|
||||
return safePurchaseExportText(item.Location.Name)
|
||||
return safePurchaseExportText(item.Warehouse.Name)
|
||||
}
|
||||
|
||||
func formatPurchaseExportStatus(item dto.PurchaseListDTO) string {
|
||||
if item.LatestApproval == nil {
|
||||
func safePurchaseItemLocationName(item *entity.PurchaseItem) string {
|
||||
if item.Warehouse == nil || item.Warehouse.Location == nil {
|
||||
return "-"
|
||||
}
|
||||
return safePurchaseExportText(item.Warehouse.Location.Name)
|
||||
}
|
||||
|
||||
func safePurchaseItemProductName(item *entity.PurchaseItem) string {
|
||||
if item.Product == nil {
|
||||
return "-"
|
||||
}
|
||||
return safePurchaseExportText(item.Product.Name)
|
||||
}
|
||||
|
||||
func formatPurchaseExportEntityStatus(purchase *entity.Purchase) string {
|
||||
if purchase.LatestApproval == nil {
|
||||
return "-"
|
||||
}
|
||||
|
||||
if item.LatestApproval.Action != nil &&
|
||||
strings.EqualFold(strings.TrimSpace(*item.LatestApproval.Action), string(entity.ApprovalActionRejected)) {
|
||||
if purchase.LatestApproval.Action != nil &&
|
||||
strings.EqualFold(strings.TrimSpace(string(*purchase.LatestApproval.Action)), string(entity.ApprovalActionRejected)) {
|
||||
return "Ditolak"
|
||||
}
|
||||
|
||||
return safePurchaseExportText(item.LatestApproval.StepName)
|
||||
return safePurchaseExportText(purchase.LatestApproval.StepName)
|
||||
}
|
||||
|
||||
func formatPurchaseExportDate(value *time.Time) string {
|
||||
@@ -273,33 +323,6 @@ func formatPurchaseExportDate(value *time.Time) string {
|
||||
return t.Format("02-01-2006")
|
||||
}
|
||||
|
||||
func formatPurchaseProducts(item dto.PurchaseListDTO) string {
|
||||
if len(item.Products) == 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
names := make([]string, 0, len(item.Products))
|
||||
for i := range item.Products {
|
||||
name := strings.TrimSpace(item.Products[i].Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[name]; exists {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
func safePurchaseExportPointerText(value *string) string {
|
||||
if value == nil {
|
||||
return "-"
|
||||
|
||||
@@ -31,6 +31,7 @@ type PurchaseListDTO struct {
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
RequesterName string `json:"requester_name"`
|
||||
PoExpedition []PoExpeditionDTO `json:"po_expedition"`
|
||||
Items []PurchaseItemDTO `json:"items"`
|
||||
Products []productDTO.ProductRelationDTO `json:"products"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location"`
|
||||
Area *areaDTO.AreaRelationDTO `json:"area"`
|
||||
@@ -227,6 +228,7 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
|
||||
CreatedUser: createdUser,
|
||||
RequesterName: requesterName,
|
||||
PoExpedition: poExpedition,
|
||||
Items: ToPurchaseItemDTOs(p.Items),
|
||||
Products: products,
|
||||
Location: location,
|
||||
Area: area,
|
||||
|
||||
Reference in New Issue
Block a user