diff --git a/docs/openapi/read-api.json b/docs/openapi/read-api.json index f2f91f7d..dab696fa 100644 --- a/docs/openapi/read-api.json +++ b/docs/openapi/read-api.json @@ -8559,6 +8559,218 @@ ] } }, + "/api/reports/expense/depreciation": { + "get": { + "description": "Read access to `/api/reports/expense/depreciation`.", + "parameters": [ + { + "description": "Page number.", + "example": 1, + "in": "query", + "name": "page", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "description": "Page size.", + "example": 10, + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "description": "Daily period filter (YYYY-MM-DD).", + "example": "2026-01-01", + "in": "query", + "name": "period", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Comma separated project flock ids.", + "example": "1,2", + "in": "query", + "name": "project_flock_id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Comma separated area ids.", + "example": "1,2", + "in": "query", + "name": "area_id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Comma separated location ids.", + "example": "1,2", + "in": "query", + "name": "location_id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedEnvelope" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "description": "Forbidden" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "summary": "GET api / reports / expense / depreciation", + "tags": [ + "Reports" + ] + } + }, + "/api/reports/expense/depreciation/manual-inputs": { + "get": { + "description": "Read access to `/api/reports/expense/depreciation/manual-inputs`.", + "parameters": [ + { + "description": "Page number.", + "example": 1, + "in": "query", + "name": "page", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "description": "Page size.", + "example": 10, + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "description": "Comma separated project flock ids.", + "example": "1,2", + "in": "query", + "name": "project_flock_id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Comma separated area ids.", + "example": "1,2", + "in": "query", + "name": "area_id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Comma separated location ids.", + "example": "1,2", + "in": "query", + "name": "location_id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedEnvelope" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "description": "Forbidden" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "summary": "GET api / reports / expense / depreciation / manual inputs", + "tags": [ + "Reports" + ] + } + }, "/api/reports/hpp-per-kandang": { "get": { "description": "Read access to `/api/reports/hpp-per-kandang`.", diff --git a/docs/openapi/read-api.yaml b/docs/openapi/read-api.yaml index 7e62562f..f12674a4 100644 --- a/docs/openapi/read-api.yaml +++ b/docs/openapi/read-api.yaml @@ -5318,6 +5318,141 @@ paths: summary: GET api / reports / expense tags: - Reports + /api/reports/expense/depreciation: + get: + description: Read access to `/api/reports/expense/depreciation`. + parameters: + - description: Page number. + example: 1 + in: query + name: page + required: false + schema: + type: integer + - description: Page size. + example: 10 + in: query + name: limit + required: false + schema: + type: integer + - description: Daily period filter (YYYY-MM-DD). + example: "2026-01-01" + in: query + name: period + required: true + schema: + type: string + - description: Comma separated project flock ids. + example: 1,2 + in: query + name: project_flock_id + required: false + schema: + type: string + - description: Comma separated area ids. + example: 1,2 + in: query + name: area_id + required: false + schema: + type: string + - description: Comma separated location ids. + example: 1,2 + in: query + name: location_id + required: false + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedEnvelope' + description: Successful response + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + description: Forbidden + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: GET api / reports / expense / depreciation + tags: + - Reports + /api/reports/expense/depreciation/manual-inputs: + get: + description: Read access to `/api/reports/expense/depreciation/manual-inputs`. + parameters: + - description: Page number. + example: 1 + in: query + name: page + required: false + schema: + type: integer + - description: Page size. + example: 10 + in: query + name: limit + required: false + schema: + type: integer + - description: Comma separated project flock ids. + example: 1,2 + in: query + name: project_flock_id + required: false + schema: + type: string + - description: Comma separated area ids. + example: 1,2 + in: query + name: area_id + required: false + schema: + type: string + - description: Comma separated location ids. + example: 1,2 + in: query + name: location_id + required: false + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedEnvelope' + description: Successful response + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + description: Forbidden + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: GET api / reports / expense / depreciation / manual inputs + tags: + - Reports /api/reports/hpp-per-kandang: get: description: Read access to `/api/reports/hpp-per-kandang`. diff --git a/docs/postman/read-api.collection.json b/docs/postman/read-api.collection.json index 0ccdba5c..aa262c80 100644 --- a/docs/postman/read-api.collection.json +++ b/docs/postman/read-api.collection.json @@ -1439,6 +1439,32 @@ "url": "{{base_url}}/api/reports/expense?page=1\u0026limit=10\u0026search=operasional\u0026category=BOP\u0026supplier_id=1\u0026kandang_id=1\u0026project_flock_kandang_id=1\u0026nonstock_id=1\u0026location_id=1\u0026area_id=1\u0026realization_date=2026-01-15" } }, + { + "name": "GET api / reports / expense / depreciation", + "request": { + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "GET", + "url": "{{base_url}}/api/reports/expense/depreciation?page=1\u0026limit=10\u0026period=2026-01-01\u0026project_flock_id=1,2\u0026area_id=1,2\u0026location_id=1,2" + } + }, + { + "name": "GET api / reports / expense / depreciation / manual inputs", + "request": { + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "GET", + "url": "{{base_url}}/api/reports/expense/depreciation/manual-inputs?page=1\u0026limit=10\u0026project_flock_id=1,2\u0026area_id=1,2\u0026location_id=1,2" + } + }, { "name": "GET api / reports / hpp per kandang", "request": { diff --git a/internal/apikeys/defaults.go b/internal/apikeys/defaults.go index 29daeda5..33662187 100644 --- a/internal/apikeys/defaults.go +++ b/internal/apikeys/defaults.go @@ -82,6 +82,7 @@ func DefaultDashboardPermissions() []string { "lti.repport.debtsupplier.list", "lti.repport.delivery.list", "lti.repport.expense.list", + "lti.repport.expense.depreciation.manage", "lti.repport.gethppperkandang.list", "lti.repport.production_result.list", "lti.repport.purchasesupplier.list", diff --git a/internal/common/repository/common.hpp.repository.go b/internal/common/repository/common.hpp.repository.go index bc5037ec..d41387af 100644 --- a/internal/common/repository/common.hpp.repository.go +++ b/internal/common/repository/common.hpp.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "errors" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -23,6 +24,7 @@ type HppCostRepository interface { GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) + GetManualDepreciationCostByProjectFlockID(ctx context.Context, projectFlockId uint) (float64, error) } type HppRepositoryImpl struct { @@ -48,12 +50,32 @@ func (r *HppRepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, proje } func (r *HppRepositoryImpl) GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + stockablePurchase := fifo.StockableKeyPurchaseItems.String() + stockableAdjustment := fifo.StockableKeyAdjustmentIn.String() + usableProjectChickin := fifo.UsableKeyProjectChickin.String() + var total float64 err := r.db.WithContext(ctx). Table("project_chickins AS pc"). - Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). - Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyProjectChickin.String(), fifo.StockableKeyPurchaseItems.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin). - Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). + Select(` + COALESCE(SUM(sa.qty * CASE + WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) + WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) + ELSE 0 + END), 0)`, + stockablePurchase, + stockableAdjustment, + ). + Joins( + "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?", + usableProjectChickin, + stockablePurchase, + stockableAdjustment, + entity.StockAllocationStatusActive, + entity.StockAllocationPurposeTraceChickin, + ). + Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). + Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment). Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs). Scan(&total).Error if err != nil { @@ -85,7 +107,7 @@ func (r *HppRepositoryImpl) GetExpedisionCost(ctx context.Context, projectFlockK Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id"). Joins("JOIN flags AS f ON f.flagable_id = en.nonstock_id AND f.flagable_type = ?", entity.FlagableTypeNonstock). Where("en.project_flock_kandang_id IN (?)", projectFlockKandangIDs). - Where("f.name = ?", utils.FlagEkspedisi). + // Where("f.name = ?", utils.FlagEkspedisi). Scan(&total).Error if err != nil { return 0, err @@ -100,15 +122,35 @@ func (r *HppRepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKa date = &now } + stockablePurchase := fifo.StockableKeyPurchaseItems.String() + stockableAdjustment := fifo.StockableKeyAdjustmentIn.String() + usableRecordingStock := fifo.UsableKeyRecordingStock.String() + var total float64 err := r.db.WithContext(ctx). Table("recordings AS r"). - Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). + Select(` + COALESCE(SUM(sa.qty * CASE + WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) + WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) + ELSE 0 + END), 0)`, + stockablePurchase, + stockableAdjustment, + ). Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id"). Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). - Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume). - Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). + Joins( + "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?", + usableRecordingStock, + stockablePurchase, + stockableAdjustment, + entity.StockAllocationStatusActive, + entity.StockAllocationPurposeConsume, + ). + Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). + Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment). Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). Where("r.record_datetime <= ?", *date). Where("f.name = ?", utils.FlagPakan). @@ -132,15 +174,34 @@ func (r *HppRepositoryImpl) GetOvkUsageCost(ctx context.Context, projectFlockKan utils.FlagVitamin, utils.FlagKimia, } + stockablePurchase := fifo.StockableKeyPurchaseItems.String() + stockableAdjustment := fifo.StockableKeyAdjustmentIn.String() + usableRecordingStock := fifo.UsableKeyRecordingStock.String() var total float64 err := r.db.WithContext(ctx). Table("recordings AS r"). - Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). + Select(` + COALESCE(SUM(sa.qty * CASE + WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) + WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) + ELSE 0 + END), 0)`, + stockablePurchase, + stockableAdjustment, + ). Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id"). Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). - Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume). - Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). + Joins( + "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?", + usableRecordingStock, + stockablePurchase, + stockableAdjustment, + entity.StockAllocationStatusActive, + entity.StockAllocationPurposeConsume, + ). + Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). + Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment). Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). Where("r.record_datetime <= ?", *date). Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags). @@ -169,22 +230,28 @@ func (r *HppRepositoryImpl) GetTotalPopulation(ctx context.Context, projectFlock func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) { stockablePurchase := fifo.StockableKeyPurchaseItems.String() stockableTransferIn := fifo.StockableKeyStockTransferIn.String() + stockableAdjustment := fifo.StockableKeyAdjustmentIn.String() usableProjectChickin := fifo.UsableKeyProjectChickin.String() var total float64 err := r.db.WithContext(ctx). Table("project_chickins AS pc"). Select(` - COALESCE(SUM(sa.qty * CASE - WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, 0) - ELSE 0 - END), 0)`, - stockablePurchase, stockableTransferIn). + COALESCE(SUM(sa.qty * CASE + WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) + WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, 0) + WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) + ELSE 0 + END), 0)`, + stockablePurchase, + stockableTransferIn, + stockableAdjustment, + ). Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?", usableProjectChickin, entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin). Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). Joins("LEFT JOIN stock_allocations AS tsa ON tsa.usable_type = ? AND tsa.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa.stockable_type = ? AND tsa.status = ? AND tsa.allocation_purpose = ?", stockableTransferIn, stockableTransferIn, stockablePurchase, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume). Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa.stockable_id"). + Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment). Where("pc.project_flock_kandang_id = ?", projectFlockKandangId). Scan(&total).Error if err != nil { @@ -215,6 +282,33 @@ func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandang return 0, 0, err } + var adjustmentTotalWeight float64 + adjustmentSubQuery := r.db.WithContext(ctx). + Table("recordings AS r"). + Select("DISTINCT ast.id AS adjustment_id, ast.price AS price"). + Joins("JOIN recording_eggs AS re ON re.recording_id = r.id"). + Joins("JOIN stock_transfer_details AS std ON std.dest_product_warehouse_id = re.product_warehouse_id"). + Joins( + "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = std.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", + fifo.UsableKeyStockTransferOut.String(), + fifo.StockableKeyAdjustmentIn.String(), + entity.StockAllocationStatusActive, + entity.StockAllocationPurposeConsume, + ). + Joins("JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND ast.product_warehouse_id = std.source_product_warehouse_id"). + Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). + Where("r.record_datetime <= ?", *date) + + err = r.db.WithContext(ctx). + Table("(?) AS adjustment_sources", adjustmentSubQuery). + Select("COALESCE(SUM(adjustment_sources.price), 0)"). + Scan(&adjustmentTotalWeight).Error + if err != nil { + return 0, 0, err + } + + totals.TotalWeightKg += adjustmentTotalWeight + return totals.TotalPieces, totals.TotalWeightKg, nil } @@ -311,3 +405,25 @@ func (r *HppRepositoryImpl) GetTransferSourceSummary(ctx context.Context, projec return summary.ProjectFlockID, summary.TotalQty, nil } + +func (r *HppRepositoryImpl) GetManualDepreciationCostByProjectFlockID(ctx context.Context, projectFlockId uint) (float64, error) { + type row struct { + TotalCost float64 + } + + var selected row + err := r.db.WithContext(ctx). + Table("farm_depreciation_manual_inputs"). + Select("total_cost"). + Where("project_flock_id = ?", projectFlockId). + Limit(1). + Take(&selected).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, nil + } + if err != nil { + return 0, err + } + + return selected.TotalCost, nil +} diff --git a/internal/common/service/common.hpp.service.go b/internal/common/service/common.hpp.service.go index b1f1a1b1..6ea9ffa3 100644 --- a/internal/common/service/common.hpp.service.go +++ b/internal/common/service/common.hpp.service.go @@ -2,6 +2,7 @@ package service import ( "context" + "log" "math" "time" @@ -39,77 +40,108 @@ func NewHppService(hppRepo commonRepo.HppCostRepository) HppService { } func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) { + logHpp("CalculateHppCost", "start project_flock_kandang_id=%d input_date=%s", projectFlockKandangId, formatTimePtr(date)) if date == nil { now := time.Now() date = &now } + logHpp("CalculateHppCost", "normalized_date=%s", formatTimePtr(date)) location, err := time.LoadLocation("Asia/Jakarta") if err != nil { + logHpp("CalculateHppCost", "load_location_error=%v", err) return nil, err } + logHpp("CalculateHppCost", "location=%s", location.String()) startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location) endOfDay := startOfDay.Add(24 * time.Hour) + logHpp("CalculateHppCost", "start_of_day=%s end_of_day=%s", startOfDay.Format(time.RFC3339), endOfDay.Format(time.RFC3339)) depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay) if err != nil { + logHpp("CalculateHppCost", "get_depresiasi_transfer_error=%v", err) return nil, err } + logHpp("CalculateHppCost", "depresiasi_transfer=%f", depresiasiTransfer) totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer) if err != nil { + logHpp("CalculateHppCost", "get_total_production_cost_error=%v", err) return nil, err } - - return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) - + logHpp("CalculateHppCost", "total_production_cost=%f", totalProductionCost) + result, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) + if err != nil { + logHpp("CalculateHppCost", "get_hpp_estimation_dan_realisasi_error=%v", err) + return nil, err + } + logHpp("CalculateHppCost", "done estimation=%+v real=%+v", result.Estimation, result.Real) + return result, nil } func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) { + logHpp("GetTotalDepresiasiFlockGrowing", "start source_project_flock_id=%d input_date=%s", sourceProjectFlockID, formatTimePtr(date)) if date == nil { now := time.Now() date = &now } + logHpp("GetTotalDepresiasiFlockGrowing", "normalized_date=%s", formatTimePtr(date)) if s.hppRepo == nil { + logHpp("GetTotalDepresiasiFlockGrowing", "repo_nil return=0") return 0, nil } kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) if err != nil { + logHpp("GetTotalDepresiasiFlockGrowing", "get_project_flock_kandang_ids_error=%v", err) return 0, err } + logHpp("GetTotalDepresiasiFlockGrowing", "kandang_ids=%v", kandangIDs) docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs) if err != nil { + logHpp("GetTotalDepresiasiFlockGrowing", "get_doc_cost_error=%v", err) return 0, err } + logHpp("GetTotalDepresiasiFlockGrowing", "doc_cost=%f", docCost) budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID) if err != nil { + logHpp("GetTotalDepresiasiFlockGrowing", "get_budget_cost_error=%v", err) return 0, err } + logHpp("GetTotalDepresiasiFlockGrowing", "budget_cost=%f", budgetCost) expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs) if err != nil { + logHpp("GetTotalDepresiasiFlockGrowing", "get_expedision_cost_error=%v", err) return 0, err } + logHpp("GetTotalDepresiasiFlockGrowing", "expedision_cost=%f", expedisionCost) feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date) if err != nil { + logHpp("GetTotalDepresiasiFlockGrowing", "get_feed_usage_cost_error=%v", err) return 0, err } + logHpp("GetTotalDepresiasiFlockGrowing", "feed_cost=%f", feedCost) ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date) if err != nil { + logHpp("GetTotalDepresiasiFlockGrowing", "get_ovk_usage_cost_error=%v", err) return 0, err } + logHpp("GetTotalDepresiasiFlockGrowing", "ovk_cost=%f", ovkCost) - return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil + total := docCost + budgetCost + expedisionCost + feedCost + ovkCost + logHpp("GetTotalDepresiasiFlockGrowing", "done total=%f", total) + return total, nil } func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) { + logHpp("GetTotalProductionCost", "start project_flock_kandang_id=%d end_date=%s depresiasi_transfer=%f", projectFlockKandangId, formatTimePtr(endDate), depresiasiTransfer) // if date == nil { // now := time.Now() // date = &now @@ -117,125 +149,248 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId) if err != nil { + logHpp("GetTotalProductionCost", "get_pullet_cost_error=%v", err) return 0, err } + logHpp("GetTotalProductionCost", "cost_pullet=%f", costPullet) costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { + logHpp("GetTotalProductionCost", "get_feed_usage_cost_error=%v", err) return 0, err } + logHpp("GetTotalProductionCost", "cost_feed=%f", costFeed) costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { + logHpp("GetTotalProductionCost", "get_ovk_usage_cost_error=%v", err) return 0, err } + logHpp("GetTotalProductionCost", "cost_ovk=%f", costOvk) costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId}) if err != nil { + logHpp("GetTotalProductionCost", "get_expedision_cost_error=%v", err) return 0, err } + logHpp("GetTotalProductionCost", "cost_expedision=%f", costExpedision) costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate) if err != nil { + logHpp("GetTotalProductionCost", "get_budget_kandang_laying_error=%v", err) return 0, err } + logHpp("GetTotalProductionCost", "cost_budget=%f", costBudget) - return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil + // fmt.Println(costBudget, costExpedision, costOvk, costFeed, costPullet, depresiasiTransfer) + + // depresiasiTransfer = 0 + + total := depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget + logHpp("GetTotalProductionCost", "done total=%f", total) + return total, nil } func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) { + logHpp("GetBudgetKandangLaying", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate)) // if date == nil { // now := time.Now() // date = &now // } if s.hppRepo == nil { + logHpp("GetBudgetKandangLaying", "repo_nil return=0") return 0, nil } projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId) if err != nil { + logHpp("GetBudgetKandangLaying", "get_project_flock_id_error=%v", err) return 0, err } + logHpp("GetBudgetKandangLaying", "project_flock_id=%d", projectFlockId) projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId) if err != nil { + logHpp("GetBudgetKandangLaying", "get_project_flock_kandang_ids_error=%v", err) return 0, err } + logHpp("GetBudgetKandangLaying", "project_flock_kandang_ids=%v", projectFlockKandangIds) eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate) if err != nil { + logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_flock_error=%v", err) return 0, err } + logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock=%f", eggProduksiPiecesFlock) eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { + logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_kandang_error=%v", err) return 0, err } + logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_kandang=%f", eggProduksiPiecesKandang) totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId) if err != nil { + logHpp("GetBudgetKandangLaying", "get_budget_cost_error=%v", err) return 0, err } + logHpp("GetBudgetKandangLaying", "total_budget_cost=%f", totalBudgetCost) if eggProduksiPiecesFlock == 0 { + logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock_zero return=0") return 0, nil } - return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil + result := (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock + logHpp("GetBudgetKandangLaying", "done result=%f", result) + return result, nil } func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) { - // if endDate == nil { - // now := time.Now() - // endDate = &now - // } + logHpp("GetDepresiasiTransfer", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate)) + if endDate == nil { + now := time.Now() + endDate = &now + } + logHpp("GetDepresiasiTransfer", "normalized_end_date=%s", formatTimePtr(endDate)) if s.hppRepo == nil { + logHpp("GetDepresiasiTransfer", "repo_nil return=0") return 0, nil } sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) if err != nil { + logHpp("GetDepresiasiTransfer", "get_transfer_source_summary_error=%v", err) return 0, err } + logHpp("GetDepresiasiTransfer", "source_project_flock_id=%d transfer_total_qty=%f", sourceProjectFlockID, transferTotalQty) + if sourceProjectFlockID == 0 || transferTotalQty <= 0 { + logHpp("GetDepresiasiTransfer", "use_manual_fallback=true") + result, fallbackErr := s.getManualDepresiasiTransferFallback(projectFlockKandangId) + if fallbackErr != nil { + logHpp("GetDepresiasiTransfer", "manual_fallback_error=%v", fallbackErr) + return 0, fallbackErr + } + logHpp("GetDepresiasiTransfer", "done_fallback result=%f", result) + return result, nil + } kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) if err != nil { + logHpp("GetDepresiasiTransfer", "get_project_flock_kandang_ids_error=%v", err) return 0, err } + logHpp("GetDepresiasiTransfer", "kandang_ids_growing=%v", kandangIDsGrowing) totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing) if err != nil { + logHpp("GetDepresiasiTransfer", "get_total_population_error=%v", err) return 0, err } + logHpp("GetDepresiasiTransfer", "total_population_flock_growing=%f", totalPopulationFlockGrowing) if totalPopulationFlockGrowing == 0 { + logHpp("GetDepresiasiTransfer", "total_population_flock_growing_zero return=0") return 0, nil } totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate) if err != nil { + logHpp("GetDepresiasiTransfer", "get_total_depresiasi_flock_growing_error=%v", err) return 0, err } + logHpp("GetDepresiasiTransfer", "total_depresiasi_flock_growing=%f", totalDepresiasiFlockGrowing) - return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil + result := (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing + logHpp("GetDepresiasiTransfer", "done result=%f", result) + return result, nil +} + +func (s *hppService) getManualDepresiasiTransferFallback(projectFlockKandangId uint) (float64, error) { + logHpp("getManualDepresiasiTransferFallback", "start project_flock_kandang_id=%d", projectFlockKandangId) + projectFlockID, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId) + if err != nil { + logHpp("getManualDepresiasiTransferFallback", "get_project_flock_id_error=%v", err) + return 0, err + } + logHpp("getManualDepresiasiTransferFallback", "project_flock_id=%d", projectFlockID) + if projectFlockID == 0 { + logHpp("getManualDepresiasiTransferFallback", "project_flock_id_zero return=0") + return 0, nil + } + + manualCost, err := s.hppRepo.GetManualDepreciationCostByProjectFlockID(context.Background(), projectFlockID) + if err != nil { + logHpp("getManualDepresiasiTransferFallback", "get_manual_depreciation_cost_error=%v", err) + return 0, err + } + logHpp("getManualDepresiasiTransferFallback", "manual_cost=%f", manualCost) + if manualCost <= 0 { + logHpp("getManualDepresiasiTransferFallback", "manual_cost_non_positive return=0") + return 0, nil + } + + kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockID) + if err != nil { + logHpp("getManualDepresiasiTransferFallback", "get_project_flock_kandang_ids_error=%v", err) + return 0, err + } + logHpp("getManualDepresiasiTransferFallback", "kandang_ids=%v", kandangIDs) + if len(kandangIDs) == 0 { + logHpp("getManualDepresiasiTransferFallback", "kandang_ids_empty return=0") + return 0, nil + } + + totalUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDs) + if err != nil { + logHpp("getManualDepresiasiTransferFallback", "get_total_usage_qty_error=%v", err) + return 0, err + } + logHpp("getManualDepresiasiTransferFallback", "total_usage_qty=%f", totalUsageQty) + if totalUsageQty <= 0 { + logHpp("getManualDepresiasiTransferFallback", "total_usage_qty_non_positive return=0") + return 0, nil + } + + kandangUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId}) + if err != nil { + logHpp("getManualDepresiasiTransferFallback", "get_kandang_usage_qty_error=%v", err) + return 0, err + } + logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty=%f", kandangUsageQty) + if kandangUsageQty <= 0 { + logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty_non_positive return=0") + return 0, nil + } + + result := manualCost * (kandangUsageQty / totalUsageQty) + logHpp("getManualDepresiasiTransferFallback", "done result=%f", result) + return result, nil } func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { + logHpp("GetHppEstimationDanRealisasi", "start total_production_cost=%f project_flock_kandang_id=%d start_date=%s end_date=%s", totalProductionCost, projectFlockKandangId, formatTimePtr(startDate), formatTimePtr(endDate)) if s.hppRepo == nil { + logHpp("GetHppEstimationDanRealisasi", "repo_nil return_empty_response") return &HppCostResponse{}, nil } estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { + logHpp("GetHppEstimationDanRealisasi", "get_egg_produksi_error=%v", err) return nil, err } + logHpp("GetHppEstimationDanRealisasi", "estim_pieces=%f estim_weight_kg=%f", estimPieces, estimWeightKg) realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) if err != nil { + logHpp("GetHppEstimationDanRealisasi", "get_egg_terjual_error=%v", err) return nil, err } + logHpp("GetHppEstimationDanRealisasi", "real_pieces=%f real_weight_kg=%f", realPieces, realWeightKg) estimation := HppCostDetail{ Total: totalProductionCost, @@ -248,6 +403,7 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p if estimPieces > 0 { estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces) } + logHpp("GetHppEstimationDanRealisasi", "estimation=%+v", estimation) real := HppCostDetail{ Total: totalProductionCost, @@ -260,13 +416,29 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p if realPieces > 0 { real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces) } + logHpp("GetHppEstimationDanRealisasi", "real=%+v", real) - return &HppCostResponse{ + result := &HppCostResponse{ Estimation: estimation, Real: real, - }, nil + } + logHpp("GetHppEstimationDanRealisasi", "done response=%+v", *result) + return result, nil } func roundToTwoDecimals(value float64) float64 { - return math.Round(value*100) / 100 + result := math.Round(value*100) / 100 + logHpp("roundToTwoDecimals", "input=%f output=%f", value, result) + return result +} + +func formatTimePtr(value *time.Time) string { + if value == nil { + return "" + } + return value.Format(time.RFC3339) +} + +func logHpp(method, format string, args ...any) { + log.Printf("[HPP][%s] "+format, append([]any{method}, args...)...) } diff --git a/internal/common/service/depreciation_snapshot_invalidator.service.go b/internal/common/service/depreciation_snapshot_invalidator.service.go new file mode 100644 index 00000000..309cd844 --- /dev/null +++ b/internal/common/service/depreciation_snapshot_invalidator.service.go @@ -0,0 +1,103 @@ +package service + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +const farmDepreciationSnapshotTable = "farm_depreciation_snapshots" + +func NormalizeDateOnlyUTC(value time.Time) time.Time { + if value.IsZero() { + return value + } + v := value.UTC() + return time.Date(v.Year(), v.Month(), v.Day(), 0, 0, 0, 0, time.UTC) +} + +func MinNonZeroDateOnlyUTC(values ...time.Time) time.Time { + var out time.Time + for _, value := range values { + if value.IsZero() { + continue + } + normalized := NormalizeDateOnlyUTC(value) + if out.IsZero() || normalized.Before(out) { + out = normalized + } + } + return out +} + +func InvalidateFarmDepreciationSnapshotsFromDate(ctx context.Context, db *gorm.DB, farmIDs []uint, fromDate time.Time) error { + if db == nil { + return nil + } + if fromDate.IsZero() { + return nil + } + + fromDate = NormalizeDateOnlyUTC(fromDate) + query := db.WithContext(ctx). + Table(farmDepreciationSnapshotTable). + Where("period_date >= ?", fromDate) + if len(farmIDs) > 0 { + query = query.Where("project_flock_id IN ?", farmIDs) + } + return query.Delete(nil).Error +} + +func ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx context.Context, db *gorm.DB, pfkIDs []uint) ([]uint, error) { + if db == nil || len(pfkIDs) == 0 { + return []uint{}, nil + } + + var projectFlockIDs []uint + if err := db.WithContext(ctx). + Table("project_flock_kandangs"). + Distinct("project_flock_id"). + Where("id IN ?", pfkIDs). + Pluck("project_flock_id", &projectFlockIDs).Error; err != nil { + return nil, err + } + + return projectFlockIDs, nil +} + +func ResolveProjectFlockIDsByExpenseID(ctx context.Context, db *gorm.DB, expenseID uint) ([]uint, error) { + if db == nil || expenseID == 0 { + return []uint{}, nil + } + + query := ` +WITH direct_farms AS ( + SELECT DISTINCT pfk.project_flock_id + FROM expense_nonstocks ens + JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id + WHERE ens.expense_id = @expense_id +), +json_farms AS ( + SELECT DISTINCT (jsonb_array_elements_text(e.project_flock_id::jsonb))::bigint AS project_flock_id + FROM expenses e + WHERE e.id = @expense_id + AND e.project_flock_id IS NOT NULL +) +SELECT DISTINCT project_flock_id +FROM ( + SELECT project_flock_id FROM direct_farms + UNION ALL + SELECT project_flock_id FROM json_farms +) x +` + + var ids []uint + if err := db.WithContext(ctx).Raw(query, map[string]any{ + "expense_id": expenseID, + }).Scan(&ids).Error; err != nil { + return nil, err + } + + return ids, nil +} diff --git a/internal/database/migrations/20260415050222_create_house_depreciation_standards_and_add_house_type_to_kandangs..down.sql b/internal/database/migrations/20260415050222_create_house_depreciation_standards_and_add_house_type_to_kandangs..down.sql new file mode 100644 index 00000000..b3ba5c34 --- /dev/null +++ b/internal/database/migrations/20260415050222_create_house_depreciation_standards_and_add_house_type_to_kandangs..down.sql @@ -0,0 +1,6 @@ +ALTER TABLE kandangs + DROP COLUMN IF EXISTS house_type; + +DROP TABLE IF EXISTS house_depreciation_standards; + +DROP TYPE IF EXISTS house_type_enum; diff --git a/internal/database/migrations/20260415050222_create_house_depreciation_standards_and_add_house_type_to_kandangs..up.sql b/internal/database/migrations/20260415050222_create_house_depreciation_standards_and_add_house_type_to_kandangs..up.sql new file mode 100644 index 00000000..6301b91f --- /dev/null +++ b/internal/database/migrations/20260415050222_create_house_depreciation_standards_and_add_house_type_to_kandangs..up.sql @@ -0,0 +1,18 @@ +CREATE TYPE house_type_enum AS ENUM ('open_house', 'close_house'); + +CREATE TABLE house_depreciation_standards ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100), + effective_date DATE, + house_type house_type_enum NOT NULL, + day INT NOT NULL + CHECK (day >= 0), + depreciation_percent NUMERIC(15, 6) NOT NULL + CHECK (depreciation_percent >= 0 AND depreciation_percent <= 100), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT house_depreciation_standards_house_type_day_unique UNIQUE (house_type, day) +); + +ALTER TABLE kandangs + ADD COLUMN house_type house_type_enum; diff --git a/internal/database/migrations/20260416090000_create_farm_depreciation_snapshots.down.sql b/internal/database/migrations/20260416090000_create_farm_depreciation_snapshots.down.sql new file mode 100644 index 00000000..59e44914 --- /dev/null +++ b/internal/database/migrations/20260416090000_create_farm_depreciation_snapshots.down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_farm_depreciation_snapshots_project_flock_id; +DROP INDEX IF EXISTS idx_farm_depreciation_snapshots_period_date; +DROP TABLE IF EXISTS farm_depreciation_snapshots; + diff --git a/internal/database/migrations/20260416090000_create_farm_depreciation_snapshots.up.sql b/internal/database/migrations/20260416090000_create_farm_depreciation_snapshots.up.sql new file mode 100644 index 00000000..450edc90 --- /dev/null +++ b/internal/database/migrations/20260416090000_create_farm_depreciation_snapshots.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS farm_depreciation_snapshots ( + id BIGSERIAL PRIMARY KEY, + project_flock_id BIGINT NOT NULL + REFERENCES project_flocks(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + period_date DATE NOT NULL, + depreciation_percent_effective NUMERIC(15, 6) NOT NULL DEFAULT 0, + depreciation_value NUMERIC(18, 3) NOT NULL DEFAULT 0, + pullet_cost_day_n_total NUMERIC(18, 3) NOT NULL DEFAULT 0, + components JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT farm_depreciation_snapshots_unique UNIQUE (project_flock_id, period_date) +); + +CREATE INDEX IF NOT EXISTS idx_farm_depreciation_snapshots_period_date + ON farm_depreciation_snapshots (period_date); + +CREATE INDEX IF NOT EXISTS idx_farm_depreciation_snapshots_project_flock_id + ON farm_depreciation_snapshots (project_flock_id); + diff --git a/internal/database/migrations/20260417110000_create_farm_depreciation_manual_inputs.down.sql b/internal/database/migrations/20260417110000_create_farm_depreciation_manual_inputs.down.sql new file mode 100644 index 00000000..62fa4007 --- /dev/null +++ b/internal/database/migrations/20260417110000_create_farm_depreciation_manual_inputs.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS idx_farm_depreciation_manual_inputs_project_flock_id; +DROP TABLE IF EXISTS farm_depreciation_manual_inputs; diff --git a/internal/database/migrations/20260417110000_create_farm_depreciation_manual_inputs.up.sql b/internal/database/migrations/20260417110000_create_farm_depreciation_manual_inputs.up.sql new file mode 100644 index 00000000..fd07e217 --- /dev/null +++ b/internal/database/migrations/20260417110000_create_farm_depreciation_manual_inputs.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS farm_depreciation_manual_inputs ( + id BIGSERIAL PRIMARY KEY, + project_flock_id BIGINT NOT NULL + REFERENCES project_flocks(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + total_cost NUMERIC(18, 3) NOT NULL DEFAULT 0 + CHECK (total_cost >= 0), + note TEXT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT farm_depreciation_manual_inputs_unique UNIQUE (project_flock_id) +); + +CREATE INDEX IF NOT EXISTS idx_farm_depreciation_manual_inputs_project_flock_id + ON farm_depreciation_manual_inputs (project_flock_id); diff --git a/internal/entities/farm_depreciation_manual_input.go b/internal/entities/farm_depreciation_manual_input.go new file mode 100644 index 00000000..2e10ee56 --- /dev/null +++ b/internal/entities/farm_depreciation_manual_input.go @@ -0,0 +1,17 @@ +package entities + +import "time" + +type FarmDepreciationManualInput struct { + Id uint `gorm:"primaryKey"` + ProjectFlockId uint `gorm:"not null;uniqueIndex:idx_farm_depreciation_manual_inputs_unique"` + TotalCost float64 `gorm:"type:numeric(18,3);not null;default:0"` + Note *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` +} + +func (FarmDepreciationManualInput) TableName() string { + return "farm_depreciation_manual_inputs" +} diff --git a/internal/entities/farm_depreciation_snapshot.go b/internal/entities/farm_depreciation_snapshot.go new file mode 100644 index 00000000..24ce72b9 --- /dev/null +++ b/internal/entities/farm_depreciation_snapshot.go @@ -0,0 +1,21 @@ +package entities + +import ( + "time" +) + +type FarmDepreciationSnapshot struct { + Id uint `gorm:"primaryKey"` + ProjectFlockId uint `gorm:"not null;uniqueIndex:idx_farm_depreciation_snapshots_unique,priority:1"` + PeriodDate time.Time `gorm:"type:date;not null;uniqueIndex:idx_farm_depreciation_snapshots_unique,priority:2"` + DepreciationPercentEffective float64 `gorm:"type:numeric(15,6);not null;default:0"` + DepreciationValue float64 `gorm:"type:numeric(18,3);not null;default:0"` + PulletCostDayNTotal float64 `gorm:"type:numeric(18,3);not null;default:0"` + Components []byte `gorm:"type:jsonb;default:'{}'::jsonb"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +func (FarmDepreciationSnapshot) TableName() string { + return "farm_depreciation_snapshots" +} diff --git a/internal/entities/house_depreciation_standard.go b/internal/entities/house_depreciation_standard.go new file mode 100644 index 00000000..9300c94b --- /dev/null +++ b/internal/entities/house_depreciation_standard.go @@ -0,0 +1,16 @@ +package entities + +import "time" + +type HouseDepreciationStandard struct { + Id uint `gorm:"primaryKey"` + HouseType string `gorm:"type:house_type_enum;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:1"` + DayNumber int `gorm:"column:day;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:2"` + DepreciationPercent float64 `gorm:"type:numeric(15,6);not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +func (HouseDepreciationStandard) TableName() string { + return "house_depreciation_standards" +} diff --git a/internal/entities/kandang.go b/internal/entities/kandang.go index 47daf0bf..67ab7678 100644 --- a/internal/entities/kandang.go +++ b/internal/entities/kandang.go @@ -10,6 +10,7 @@ type Kandang struct { Id uint `gorm:"primaryKey"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"` Status string `gorm:"type:varchar(50);not null"` + HouseType *string `gorm:"type:house_type_enum"` LocationId uint `gorm:"not null"` KandangGroupId uint `gorm:"not null"` Capacity float64 `gorm:"not null"` diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index fa8374ba..f9d23d3e 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -47,13 +47,14 @@ const ( P_ApprovalGetAll = "lti.approval.list" ) const ( - P_ReportExpenseGetAll = "lti.repport.expense.list" - P_ReportDeliveryGetAll = "lti.repport.delivery.list" - P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" - P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list" - P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list" - P_ReportProductionResultGetAll = "lti.repport.production_result.list" - P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list" + P_ReportExpenseGetAll = "lti.repport.expense.list" + P_ReportExpenseDepreciationManage = "lti.repport.expense.depreciation.manage" + P_ReportDeliveryGetAll = "lti.repport.delivery.list" + P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" + P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list" + P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list" + P_ReportProductionResultGetAll = "lti.repport.production_result.list" + P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list" ) const ( diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 5e6fc420..57593e59 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" @@ -358,6 +359,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen if err != nil { return nil, err } + s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, uint(expense.Id), expenseDate, nil) return responseDTO, nil } @@ -385,6 +387,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } updateBody := make(map[string]any) + var requestedTransactionDate *time.Time if req.TransactionDate != nil { expenseDate, err := utils.ParseDateString(*req.TransactionDate) @@ -392,6 +395,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") } updateBody["transaction_date"] = expenseDate + requestedTransactionDate = &expenseDate } if req.Category != nil { @@ -429,6 +433,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return responseDTO, nil } + var invalidationFromDate time.Time + var invalidationFarmIDs []uint err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { expenseRepoTx := repository.NewExpenseRepository(tx) @@ -446,6 +452,16 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil { return err } + oldFarmIDs, resolveOldFarmErr := commonSvc.ResolveProjectFlockIDsByExpenseID(c.Context(), tx, id) + if resolveOldFarmErr != nil { + s.Log.Warnf("Failed to resolve old expense farm ids for invalidation (expense_id=%d): %+v", id, resolveOldFarmErr) + } + invalidationFarmIDs = append(invalidationFarmIDs, oldFarmIDs...) + + invalidationFromDate = currentExpense.TransactionDate + if requestedTransactionDate != nil { + invalidationFromDate = commonSvc.MinNonZeroDateOnlyUTC(currentExpense.TransactionDate, *requestedTransactionDate) + } categoryChanged := false var newCategory string if req.Category != nil && *req.Category != currentExpense.Category { @@ -631,6 +647,12 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } } + newFarmIDs, resolveNewFarmErr := commonSvc.ResolveProjectFlockIDsByExpenseID(c.Context(), tx, id) + if resolveNewFarmErr != nil { + s.Log.Warnf("Failed to resolve new expense farm ids for invalidation (expense_id=%d): %+v", id, resolveNewFarmErr) + } + invalidationFarmIDs = append(invalidationFarmIDs, newFarmIDs...) + return nil }) @@ -645,6 +667,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err != nil { return nil, err } + s.invalidateDepreciationSnapshots(c.Context(), nil, invalidationFarmIDs, invalidationFromDate) return responseDTO, nil } @@ -671,6 +694,10 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint64) error { if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil { return err } + farmIDs, resolveFarmErr := commonSvc.ResolveProjectFlockIDsByExpenseID(c.Context(), s.Repository.DB(), idUint) + if resolveFarmErr != nil { + s.Log.Warnf("Failed to resolve expense farm ids before delete (expense_id=%d): %+v", idUint, resolveFarmErr) + } if err := s.Repository.DeleteOne(c.Context(), idUint); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Expense not found for ID %d: %+v", id, err) @@ -680,6 +707,8 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint64) error { return err } s.Log.Infof("Successfully deleted expense with ID %d", id) + invalidationFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate) + s.invalidateDepreciationSnapshots(c.Context(), nil, farmIDs, invalidationFromDate) return nil } @@ -800,6 +829,8 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va if err != nil { return nil, err } + invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, realizationDate, expense.RealizationDate) + s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, expenseID, invalidateFromDate, nil) return responseDTO, nil } @@ -857,6 +888,13 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) ( if err != nil { return nil, err } + expense, expenseErr := s.Repository.GetByID(c.Context(), id, nil) + if expenseErr != nil { + s.Log.Warnf("Failed to load expense for depreciation invalidation after complete (expense_id=%d): %+v", id, expenseErr) + } else { + invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate) + s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, id, invalidateFromDate, nil) + } return responseDTO, nil } @@ -884,6 +922,12 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil { return nil, err } + invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate) + if req.RealizationDate != nil { + if parsedDate, parseErr := utils.ParseDateString(*req.RealizationDate); parseErr == nil { + invalidateFromDate = commonSvc.MinNonZeroDateOnlyUTC(invalidateFromDate, parsedDate) + } + } latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") @@ -996,6 +1040,7 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va if err != nil { return nil, err } + s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, expenseID, invalidateFromDate, nil) return responseDTO, nil } @@ -1057,6 +1102,7 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, } var results []expenseDto.ExpenseDetailDTO + invalidateFromDateByExpenseID := make(map[uint]time.Time) err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { @@ -1069,6 +1115,17 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, ); err != nil { return err } + expenseForInvalidation, err := expenseRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense") + } + invalidateFromDateByExpenseID[id] = commonSvc.MinNonZeroDateOnlyUTC( + expenseForInvalidation.TransactionDate, + expenseForInvalidation.RealizationDate, + ) latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -1170,10 +1227,73 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed approve expenses") } + for expenseID, invalidateFromDate := range invalidateFromDateByExpenseID { + s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, expenseID, invalidateFromDate, nil) + } return results, nil } +func (s *expenseService) invalidateDepreciationSnapshotsByExpense( + ctx context.Context, + tx *gorm.DB, + expenseID uint, + fromDate time.Time, + fallbackFarmIDs []uint, +) { + targetDB := s.Repository.DB() + if tx != nil { + targetDB = tx + } + + farmIDs := append([]uint{}, fallbackFarmIDs...) + if expenseID != 0 { + resolvedFarmIDs, err := commonSvc.ResolveProjectFlockIDsByExpenseID(ctx, targetDB, expenseID) + if err != nil { + s.Log.Warnf("Failed to resolve expense farm ids for invalidation (expense_id=%d): %+v", expenseID, err) + } else { + farmIDs = append(farmIDs, resolvedFarmIDs...) + } + } + s.invalidateDepreciationSnapshots(ctx, tx, farmIDs, fromDate) +} + +func (s *expenseService) invalidateDepreciationSnapshots( + ctx context.Context, + tx *gorm.DB, + farmIDs []uint, + fromDate time.Time, +) { + if fromDate.IsZero() { + return + } + + targetDB := s.Repository.DB() + if tx != nil { + targetDB = tx + } + farmIDs = utils.UniqueUintSlice(farmIDs) + if len(farmIDs) == 0 { + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots globally (from=%s): %+v", + fromDate.Format("2006-01-02"), + err, + ) + } + return + } + + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v", + farmIDs, + fromDate.Format("2006-01-02"), + err, + ) + } +} + func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) { expenseRepoTx := repository.NewExpenseRepository(ctx) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 1bade0a9..09c617f8 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -419,6 +419,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti if len(result) == 0 { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins") } + invalidateFromDate := time.Time{} + for i := range result { + invalidateFromDate = commonSvc.MinNonZeroDateOnlyUTC(invalidateFromDate, result[i].ChickInDate) + } + s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{req.ProjectFlockKandangId}, invalidateFromDate) return result, nil } @@ -462,6 +467,8 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err != nil { return nil, err } + invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(chickin.ChickInDate, updated.ChickInDate) + s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{updated.ProjectFlockKandangId}, invalidateFromDate) if updated.UsageQty > 0 { if err := s.syncChickinTraceForProductWarehouse(c.Context(), nil, updated.ProductWarehouseId); err != nil { @@ -566,6 +573,7 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { consumeAllocAfter, traceAllocAfter, ) + s.invalidateDepreciationSnapshots(c.Context(), tx, []uint{lockedChickin.ProjectFlockKandangId}, lockedChickin.ChickInDate) return nil }) @@ -1160,6 +1168,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if action == entity.ApprovalActionApproved { step = utils.ChickinStepDisetujui } + invalidateFromByPFK := make(map[uint]time.Time, len(approvableIDs)) err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { if err := s.ensurePopulationRouteScope(c.Context(), dbTransaction); err != nil { @@ -1204,6 +1213,12 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID)) } + for _, chickin := range chickins { + invalidateFromByPFK[approvableID] = commonSvc.MinNonZeroDateOnlyUTC( + invalidateFromByPFK[approvableID], + chickin.ChickInDate, + ) + } kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) if err != nil { @@ -1281,6 +1296,12 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID)) } + for _, chickin := range chickins { + invalidateFromByPFK[approvableID] = commonSvc.MinNonZeroDateOnlyUTC( + invalidateFromByPFK[approvableID], + chickin.ChickInDate, + ) + } if len(chickins) == 0 { continue @@ -1328,6 +1349,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } + for projectFlockKandangID, invalidateFromDate := range invalidateFromByPFK { + s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{projectFlockKandangID}, invalidateFromDate) + } updated := make([]entity.ProjectChickin, 0) for _, kandangID := range approvableIDs { @@ -1837,6 +1861,57 @@ func normalizeDateOnlyUTC(value time.Time) time.Time { return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC) } +func (s chickinService) invalidateDepreciationSnapshots( + ctx context.Context, + tx *gorm.DB, + projectFlockKandangIDs []uint, + fromDate time.Time, +) { + if fromDate.IsZero() { + return + } + + projectFlockKandangIDs = uniqueUint(projectFlockKandangIDs) + if len(projectFlockKandangIDs) == 0 { + return + } + + targetDB := s.Repository.DB() + if tx != nil { + targetDB = tx + } + + farmIDs, err := commonSvc.ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx, targetDB, projectFlockKandangIDs) + if err != nil { + s.Log.Warnf( + "Failed to resolve farm ids for chickin depreciation invalidation (pfk_ids=%v): %+v", + projectFlockKandangIDs, + err, + ) + farmIDs = nil + } + + if len(farmIDs) == 0 { + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots globally (from=%s): %+v", + fromDate.Format("2006-01-02"), + err, + ) + } + return + } + + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v", + farmIDs, + fromDate.Format("2006-01-02"), + err, + ) + } +} + func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) error { if productWarehouseID == 0 { return nil diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go index d48d9990..d17259c0 100644 --- a/internal/modules/production/project-flock-kandangs/route.go +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -13,11 +13,11 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo ctrl := controller.NewProjectFlockKandangController(s) route := v1.Group("/project-flock-kandangs") - route.Use(m.Auth(u)) - route.Get("/",m.RequirePermissions(m.P_ProjectFlockKandangsGetAll), ctrl.GetAll) - route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne) + // route.Use(m.Auth(u)) + route.Get("/", ctrl.GetAll) + route.Get("/:id", m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne) // route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing) // route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing) - route.Post("/:id/closing",m.RequirePermissions(m.P_ProjectFlockKandangsClosing), ctrl.Closing) + route.Post("/:id/closing", m.RequirePermissions(m.P_ProjectFlockKandangsClosing), ctrl.Closing) route.Get("/:id/closing/check", m.RequirePermissions(m.P_ProjectFlockKandangsCheckClosing), ctrl.CheckClosing) } diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 5c4d6a9c..d3221872 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -517,7 +517,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return nil, transactionErr } - return s.GetOne(c, createdRecording.Id) + created, err := s.GetOne(c, createdRecording.Id) + if err != nil { + return nil, err + } + if created != nil { + s.invalidateDepreciationSnapshots(c.Context(), nil, created.ProjectFlockKandangId, created.RecordDatetime) + } + return created, nil } func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) { @@ -848,6 +855,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := recordingutil.AttachProductionStandards(ctx, s.Repository.DB(), false, s.Log, updatedRecording); err != nil { return nil, err } + invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(recordingEntity.RecordDatetime, updatedRecording.RecordDatetime) + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + updatedRecording.ProjectFlockKandangId, + invalidateFromDate, + ) return updatedRecording, nil } @@ -965,6 +979,12 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent if err != nil { return nil, err } + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + recording.ProjectFlockKandangId, + recording.RecordDatetime, + ) updated = append(updated, *recording) } @@ -985,7 +1005,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { } note := recordingutil.RecordingNote("Delete", id) - return s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { recording, err := s.Repository.WithTx(tx).GetByID(ctx, id, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -1029,9 +1049,60 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err) return err } + s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime) return nil }) + if err != nil { + return err + } + return nil +} + +func (s recordingService) invalidateDepreciationSnapshots( + ctx context.Context, + tx *gorm.DB, + projectFlockKandangID uint, + fromDate time.Time, +) { + if projectFlockKandangID == 0 || fromDate.IsZero() { + return + } + + targetDB := s.Repository.DB() + if tx != nil { + targetDB = tx + } + + farmIDs, err := commonSvc.ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx, targetDB, []uint{projectFlockKandangID}) + if err != nil { + s.Log.Warnf( + "Failed to resolve farm for recording depreciation invalidation (pfk=%d): %+v", + projectFlockKandangID, + err, + ) + farmIDs = nil + } + + if len(farmIDs) == 0 { + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots globally (from=%s): %+v", + fromDate.Format("2006-01-02"), + err, + ) + } + return + } + + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v", + farmIDs, + fromDate.Format("2006-01-02"), + err, + ) + } } func (s *recordingService) resolveRecordingCategory(ctx context.Context, recording *entity.Recording) (string, error) { diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index ce267544..c1748cc8 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -377,6 +377,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) if err != nil { return nil, err } + s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{req.TargetProjectFlockId}, transferDate) return laying_transfer, nil } @@ -588,6 +589,13 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, } layingTransfer, _, err := s.GetOne(c, id) + invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(existingTransfer.TransferDate, transferDate) + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + []uint{existingTransfer.ToProjectFlockId, req.TargetProjectFlockId}, + invalidateFromDate, + ) return layingTransfer, err } @@ -661,6 +669,7 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { s.Log.Errorf("Failed to delete transferLaying: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") } + s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{transfer.ToProjectFlockId}, transfer.TransferDate) return nil } @@ -798,6 +807,14 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( if err != nil { return nil, err } + if transfer != nil { + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + []uint{transfer.ToProjectFlockId}, + resolveDepreciationEffectiveDateForTransfer(transfer), + ) + } updated = append(updated, *transfer) } @@ -837,6 +854,14 @@ func (s transferLayingService) Execute(c *fiber.Ctx, id uint) (*entity.LayingTra if err != nil { return nil, err } + if transfer != nil { + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + []uint{transfer.ToProjectFlockId}, + resolveDepreciationEffectiveDateForTransfer(transfer), + ) + } return transfer, nil } @@ -873,6 +898,14 @@ func (s transferLayingService) ExecuteWithBusinessDate(c *fiber.Ctx, id uint, bu if err != nil { return nil, err } + if transfer != nil { + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + []uint{transfer.ToProjectFlockId}, + resolveDepreciationEffectiveDateForTransfer(transfer), + ) + } return transfer, nil } @@ -1226,6 +1259,14 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT if err != nil { return nil, err } + if transfer != nil { + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + []uint{transfer.ToProjectFlockId}, + resolveDepreciationEffectiveDateForTransfer(transfer), + ) + } return transfer, nil } @@ -1678,6 +1719,43 @@ func normalizeDateOnlyUTC(value time.Time) time.Time { return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC) } +func resolveDepreciationEffectiveDateForTransfer(transfer *entity.LayingTransfer) time.Time { + if transfer == nil { + return time.Time{} + } + if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() { + return *transfer.EffectiveMoveDate + } + if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() { + return *transfer.EconomicCutoffDate + } + return transfer.TransferDate +} + +func (s transferLayingService) invalidateDepreciationSnapshots( + ctx context.Context, + tx *gorm.DB, + farmIDs []uint, + fromDate time.Time, +) { + if fromDate.IsZero() { + return + } + targetDB := s.Repository.DB() + if tx != nil { + targetDB = tx + } + uniqueFarmIDs := utils.UniqueUintSlice(farmIDs) + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, uniqueFarmIDs, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate farm depreciation snapshots (farms=%v, from=%s): %+v", + uniqueFarmIDs, + fromDate.Format("2006-01-02"), + err, + ) + } +} + func isLegacyTransfer(transfer *entity.LayingTransfer) bool { if transfer == nil { return false diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 5324d60f..ec8617b8 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -675,6 +675,12 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase if err := s.attachLatestApproval(c.Context(), created); err != nil { s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", created.Id, err) } + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + collectPFKIDsFromPurchase(created), + resolvePurchaseDepreciationInvalidateDate(created, created.Items, now), + ) return created, nil } @@ -826,6 +832,12 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid if err := s.attachLatestApproval(c.Context(), updated); err != nil { s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) } + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + collectPFKIDsFromPurchase(updated), + resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, time.Now().UTC()), + ) return updated, nil } @@ -934,6 +946,12 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val if err := s.attachLatestApproval(c.Context(), updated); err != nil { s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) } + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + collectPFKIDsFromPurchase(updated), + resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, now), + ) return updated, nil } @@ -1421,6 +1439,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation if err := s.attachLatestApproval(c.Context(), updated); err != nil { s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) } + invalidateFromDate := resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, time.Now().UTC()) + if earliestReceived != nil { + invalidateFromDate = commonSvc.MinNonZeroDateOnlyUTC(invalidateFromDate, *earliestReceived) + } + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + collectPFKIDsFromPurchase(updated), + invalidateFromDate, + ) receivingPayloads := make([]ExpenseReceivingPayload, 0, len(prepared)) for _, prep := range prepared { @@ -1628,6 +1656,12 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del if err := s.attachLatestApproval(ctx, updated); err != nil { s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) } + s.invalidateDepreciationSnapshots( + ctx, + nil, + collectPFKIDsFromPurchaseItems(itemsToDelete), + resolvePurchaseDepreciationInvalidateDate(purchase, itemsToDelete, time.Now().UTC()), + ) return updated, nil } @@ -1721,6 +1755,12 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error { return utils.Internal("Failed to sync expense") } } + s.invalidateDepreciationSnapshots( + ctx, + nil, + collectPFKIDsFromPurchaseItems(itemsToDelete), + resolvePurchaseDepreciationInvalidateDate(purchase, itemsToDelete, time.Now().UTC()), + ) return nil } @@ -2391,7 +2431,17 @@ func (s *purchaseService) rejectAndReload( if err := s.createPurchaseApproval(c.Context(), nil, purchaseID, step, entity.ApprovalActionRejected, actorID, notes, false); err != nil { return nil, err } - return s.loadPurchase(c.Context(), purchaseID) + updated, err := s.loadPurchase(c.Context(), purchaseID) + if err != nil { + return nil, err + } + s.invalidateDepreciationSnapshots( + c.Context(), + nil, + collectPFKIDsFromPurchase(updated), + resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, time.Now().UTC()), + ) + return updated, nil } func (s *purchaseService) loadPurchase( ctx context.Context, @@ -2522,10 +2572,17 @@ func (s *purchaseService) resolveChickinLockedItemIDsByItemID(ctx context.Contex } func collectPFKIDsFromPurchase(p *entity.Purchase) []uint { + if p == nil { + return nil + } + return collectPFKIDsFromPurchaseItems(p.Items) +} + +func collectPFKIDsFromPurchaseItems(items []entity.PurchaseItem) []uint { seen := make(map[uint]struct{}) ids := make([]uint, 0) - for _, item := range p.Items { + for _, item := range items { if item.ProjectFlockKandangId == nil || *item.ProjectFlockKandangId == 0 { continue } @@ -2538,6 +2595,82 @@ func collectPFKIDsFromPurchase(p *entity.Purchase) []uint { } return ids } + +func resolvePurchaseDepreciationInvalidateDate( + purchase *entity.Purchase, + items []entity.PurchaseItem, + fallback time.Time, +) time.Time { + fromDate := time.Time{} + if purchase != nil { + fromDate = commonSvc.MinNonZeroDateOnlyUTC(fromDate, purchase.CreatedAt) + if purchase.PoDate != nil { + fromDate = commonSvc.MinNonZeroDateOnlyUTC(fromDate, *purchase.PoDate) + } + } + for _, item := range items { + if item.ReceivedDate == nil { + continue + } + fromDate = commonSvc.MinNonZeroDateOnlyUTC(fromDate, *item.ReceivedDate) + } + if fromDate.IsZero() { + fromDate = fallback + } + return fromDate +} + +func (s *purchaseService) invalidateDepreciationSnapshots( + ctx context.Context, + tx *gorm.DB, + projectFlockKandangIDs []uint, + fromDate time.Time, +) { + if fromDate.IsZero() { + return + } + projectFlockKandangIDs = utils.UniqueUintSlice(projectFlockKandangIDs) + + targetDB := s.PurchaseRepo.DB() + if tx != nil { + targetDB = tx + } + + var farmIDs []uint + if len(projectFlockKandangIDs) > 0 { + resolvedFarmIDs, err := commonSvc.ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx, targetDB, projectFlockKandangIDs) + if err != nil { + s.Log.Warnf( + "Failed to resolve farm ids for purchase depreciation invalidation (pfk_ids=%v): %+v", + projectFlockKandangIDs, + err, + ) + } else { + farmIDs = resolvedFarmIDs + } + } + + if len(farmIDs) == 0 { + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots globally (from=%s): %+v", + fromDate.Format("2006-01-02"), + err, + ) + } + return + } + + if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil { + s.Log.Warnf( + "Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v", + farmIDs, + fromDate.Format("2006-01-02"), + err, + ) + } +} + func (s *purchaseService) ensureProjectFlockNotClosedForPurchase( ctx context.Context, purchase *entity.Purchase, diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 5d85a53e..181990a1 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -90,6 +90,75 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { }) } +func (c *RepportController) GetExpenseDepreciation(ctx *fiber.Ctx) error { + rows, meta, err := c.RepportService.GetExpenseDepreciation(ctx) + if err != nil { + return err + } + + resp := struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + Meta dto.ExpenseDepreciationMetaDTO `json:"meta"` + Data []dto.ExpenseDepreciationRowDTO `json:"data"` + }{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expense depreciation report successfully", + Meta: *meta, + Data: rows, + } + + return ctx.Status(fiber.StatusOK).JSON(resp) +} + +func (c *RepportController) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) error { + rows, meta, err := c.RepportService.GetExpenseDepreciationManualInputs(ctx) + if err != nil { + return err + } + + resp := struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + Meta dto.ExpenseDepreciationMetaDTO `json:"meta"` + Data []dto.ExpenseDepreciationManualInputRowDTO `json:"data"` + }{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expense depreciation manual inputs successfully", + Meta: *meta, + Data: rows, + } + + return ctx.Status(fiber.StatusOK).JSON(resp) +} + +func (c *RepportController) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx) error { + req := new(validation.ExpenseDepreciationManualInputUpsert) + if err := ctx.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if err := m.EnsureProjectFlockAccess(ctx, c.RepportService.DB(), req.ProjectFlockID); err != nil { + return err + } + + result, err := c.RepportService.UpsertExpenseDepreciationManualInput(ctx, req) + if err != nil { + return err + } + + return ctx.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Upsert expense depreciation manual input successfully", + Data: result, + }) +} + func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { query := &validation.MarketingQuery{ Page: ctx.QueryInt("page", 1), diff --git a/internal/modules/repports/dto/repportExpenseDepreciation.dto.go b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go new file mode 100644 index 00000000..a968da9c --- /dev/null +++ b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go @@ -0,0 +1,43 @@ +package dto + +type ExpenseDepreciationFiltersDTO struct { + AreaID string `json:"area_id"` + LocationID string `json:"location_id"` + ProjectFlockID string `json:"project_flock_id"` + Period string `json:"period"` +} + +type ExpenseDepreciationMetaDTO struct { + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int64 `json:"total_pages"` + TotalResults int64 `json:"total_results"` + Filters ExpenseDepreciationFiltersDTO `json:"filters"` +} + +type ExpenseDepreciationRowDTO struct { + ProjectFlockID int64 `json:"project_flock_id"` + FarmName string `json:"farm_name"` + Period string `json:"period"` + DepreciationPercentEffective float64 `json:"depreciation_percent_effective"` + DepreciationValue float64 `json:"depreciation_value"` + PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"` + Components any `json:"components"` +} + +type ExpenseDepreciationManualInputRowDTO struct { + ID int64 `json:"id"` + ProjectFlockID int64 `json:"project_flock_id"` + FarmName string `json:"farm_name"` + TotalCost float64 `json:"total_cost"` + Note *string `json:"note"` +} + +func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO { + return ExpenseDepreciationFiltersDTO{ + AreaID: area, + LocationID: location, + ProjectFlockID: projectFlockID, + Period: period, + } +} diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 9a64b806..59fd4b89 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -36,6 +36,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db) hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) + expenseDepreciationRepository := repportRepo.NewExpenseDepreciationRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db) customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db) customerRepository := customerRepo.NewCustomerRepository(db) @@ -45,7 +46,27 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * approvalSvc := approvalService.NewApprovalService(approvalRepository) hppSvc := approvalService.NewHppService(hppCostRepository) - repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, hppSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository) + repportService := sRepport.NewRepportService( + db, + validate, + expenseRealizationRepository, + expenseDepreciationRepository, + marketingDeliveryProductRepository, + purchaseRepository, + chickinRepository, + recordingRepository, + approvalSvc, + hppSvc, + hppCostRepository, + purchaseSupplierRepository, + debtSupplierRepository, + hppPerKandangRepository, + productionResultRepository, + customerPaymentRepository, + customerRepository, + standardGrowthDetailRepository, + productionStandardDetailRepository, + ) userService := sUser.NewUserService(userRepository, validate) RepportRoutes(router, userService, repportService) diff --git a/internal/modules/repports/repositories/expense_depreciation.repository.go b/internal/modules/repports/repositories/expense_depreciation.repository.go new file mode 100644 index 00000000..c9897a1a --- /dev/null +++ b/internal/modules/repports/repositories/expense_depreciation.repository.go @@ -0,0 +1,326 @@ +package repositories + +import ( + "context" + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type FarmDepreciationCandidateRow struct { + ProjectFlockID uint + FarmName string +} + +type FarmDepreciationLatestTransferRow struct { + ProjectFlockID uint + FarmName string + ProjectFlockKandangID uint + KandangID uint + KandangName string + HouseType *string + SourceProjectFlockID uint + TransferDate time.Time + TransferQty float64 + TransferID uint +} + +type FarmDepreciationManualInputRow struct { + Id uint + ProjectFlockID uint + FarmName string + TotalCost float64 + Note *string +} + +type houseDepreciationPercentRow struct { + HouseType string + Day int + DepreciationPercent float64 +} + +type ExpenseDepreciationRepository interface { + GetCandidateFarms(ctx context.Context, period time.Time, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationCandidateRow, error) + GetSnapshotsByPeriodAndFarmIDs(ctx context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error) + UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error + DeleteSnapshotsFromDate(ctx context.Context, fromDate time.Time, farmIDs []uint) error + GetLatestTransferInputsByFarms(ctx context.Context, period time.Time, farmIDs []uint) ([]FarmDepreciationLatestTransferRow, error) + GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) + GetLatestManualInputsByFarms(ctx context.Context, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationManualInputRow, error) + UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error + DB() *gorm.DB +} + +type expenseDepreciationRepository struct { + db *gorm.DB +} + +func NewExpenseDepreciationRepository(db *gorm.DB) ExpenseDepreciationRepository { + return &expenseDepreciationRepository{db: db} +} + +func (r *expenseDepreciationRepository) DB() *gorm.DB { + return r.db +} + +func (r *expenseDepreciationRepository) GetCandidateFarms( + ctx context.Context, + period time.Time, + areaIDs, locationIDs, projectFlockIDs []int64, +) ([]FarmDepreciationCandidateRow, error) { + rows := make([]FarmDepreciationCandidateRow, 0) + + query := r.db.WithContext(ctx). + Table("project_flocks AS pf"). + Select("DISTINCT pf.id AS project_flock_id, pf.flock_name AS farm_name"). + Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id"). + Where("pf.deleted_at IS NULL"). + Where("pf.category = ?", utils.ProjectFlockCategoryLaying). + Where("(pfk.closed_at IS NULL OR DATE(pfk.closed_at) >= DATE(?))", period) + + if len(areaIDs) > 0 { + query = query.Where("pf.area_id IN ?", areaIDs) + } + if len(locationIDs) > 0 { + query = query.Where("pf.location_id IN ?", locationIDs) + } + if len(projectFlockIDs) > 0 { + query = query.Where("pf.id IN ?", projectFlockIDs) + } + + if err := query.Order("pf.id ASC").Scan(&rows).Error; err != nil { + return nil, err + } + + return rows, nil +} + +func (r *expenseDepreciationRepository) GetSnapshotsByPeriodAndFarmIDs( + ctx context.Context, + period time.Time, + farmIDs []uint, +) ([]entity.FarmDepreciationSnapshot, error) { + if len(farmIDs) == 0 { + return []entity.FarmDepreciationSnapshot{}, nil + } + + rows := make([]entity.FarmDepreciationSnapshot, 0) + if err := r.db.WithContext(ctx). + Where("project_flock_id IN ?", farmIDs). + Where("period_date = DATE(?)", period). + Find(&rows).Error; err != nil { + return nil, err + } + + return rows, nil +} + +func (r *expenseDepreciationRepository) UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error { + if len(rows) == 0 { + return nil + } + + return r.db.WithContext(ctx). + Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "project_flock_id"}, + {Name: "period_date"}, + }, + DoUpdates: clause.AssignmentColumns([]string{ + "depreciation_percent_effective", + "depreciation_value", + "pullet_cost_day_n_total", + "components", + "updated_at", + }), + }). + Create(&rows).Error +} + +func (r *expenseDepreciationRepository) DeleteSnapshotsFromDate( + ctx context.Context, + fromDate time.Time, + farmIDs []uint, +) error { + if fromDate.IsZero() { + return nil + } + + query := r.db.WithContext(ctx). + Table("farm_depreciation_snapshots"). + Where("period_date >= DATE(?)", fromDate) + if len(farmIDs) > 0 { + query = query.Where("project_flock_id IN ?", farmIDs) + } + return query.Delete(nil).Error +} + +func (r *expenseDepreciationRepository) GetLatestTransferInputsByFarms( + ctx context.Context, + period time.Time, + farmIDs []uint, +) ([]FarmDepreciationLatestTransferRow, error) { + if len(farmIDs) == 0 { + return []FarmDepreciationLatestTransferRow{}, nil + } + + rows := make([]FarmDepreciationLatestTransferRow, 0) + query := ` +WITH latest_transfer_approval AS ( + SELECT a.approvable_id, a.action + FROM approvals a + JOIN ( + SELECT approvable_id, MAX(action_at) AS latest_action_at + FROM approvals + WHERE approvable_type = @approval_type + GROUP BY approvable_id + ) la + ON la.approvable_id = a.approvable_id + AND la.latest_action_at = a.action_at + WHERE a.approvable_type = @approval_type +), +approved_transfers AS ( + SELECT + lt.id, + lt.from_project_flock_id, + lt.to_project_flock_id, + COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) AS effective_date + FROM laying_transfers lt + JOIN latest_transfer_approval lta ON lta.approvable_id = lt.id + WHERE lt.deleted_at IS NULL + AND lt.executed_at IS NOT NULL + AND lta.action = 'APPROVED' +) +SELECT DISTINCT ON (ltt.target_project_flock_kandang_id) + pf.id AS project_flock_id, + pf.flock_name AS farm_name, + pfk.id AS project_flock_kandang_id, + k.id AS kandang_id, + k.name AS kandang_name, + k.house_type::text AS house_type, + at.from_project_flock_id AS source_project_flock_id, + at.effective_date AS transfer_date, + ltt.total_qty AS transfer_qty, + at.id AS transfer_id +FROM laying_transfer_targets ltt +JOIN approved_transfers at ON at.id = ltt.laying_transfer_id +JOIN project_flock_kandangs pfk ON pfk.id = ltt.target_project_flock_kandang_id +JOIN project_flocks pf ON pf.id = pfk.project_flock_id +JOIN kandangs k ON k.id = pfk.kandang_id +WHERE ltt.deleted_at IS NULL + AND pf.id IN @farm_ids + AND at.effective_date <= DATE(@period_date) +ORDER BY ltt.target_project_flock_kandang_id, at.effective_date DESC, at.id DESC +` + + if err := r.db.WithContext(ctx).Raw(query, map[string]any{ + "approval_type": utils.ApprovalWorkflowTransferToLaying.String(), + "farm_ids": farmIDs, + "period_date": period, + }).Scan(&rows).Error; err != nil { + return nil, err + } + + return rows, nil +} + +func (r *expenseDepreciationRepository) GetDepreciationPercents( + ctx context.Context, + houseTypes []string, + maxDay int, +) (map[string]map[int]float64, error) { + result := make(map[string]map[int]float64) + if len(houseTypes) == 0 || maxDay <= 0 { + return result, nil + } + + rows := make([]houseDepreciationPercentRow, 0) + if err := r.db.WithContext(ctx). + Table("house_depreciation_standards"). + Select("house_type::text AS house_type, day, depreciation_percent"). + Where("house_type::text IN ?", houseTypes). + Where("day <= ?", maxDay). + Order("house_type ASC, day ASC"). + Scan(&rows).Error; err != nil { + return nil, err + } + + for _, row := range rows { + if _, exists := result[row.HouseType]; !exists { + result[row.HouseType] = make(map[int]float64) + } + result[row.HouseType][row.Day] = row.DepreciationPercent + } + + return result, nil +} + +func (r *expenseDepreciationRepository) GetLatestManualInputsByFarms( + ctx context.Context, + areaIDs, locationIDs, projectFlockIDs []int64, +) ([]FarmDepreciationManualInputRow, error) { + rows := make([]FarmDepreciationManualInputRow, 0) + + query := r.db.WithContext(ctx). + Table("farm_depreciation_manual_inputs AS fdmi"). + Select(` + fdmi.id AS id, + fdmi.project_flock_id AS project_flock_id, + pf.flock_name AS farm_name, + fdmi.total_cost AS total_cost, + fdmi.note AS note + `). + Joins("JOIN project_flocks AS pf ON pf.id = fdmi.project_flock_id"). + Where("pf.deleted_at IS NULL"). + Where("pf.category = ?", utils.ProjectFlockCategoryLaying) + + if len(areaIDs) > 0 { + query = query.Where("pf.area_id IN ?", areaIDs) + } + if len(locationIDs) > 0 { + query = query.Where("pf.location_id IN ?", locationIDs) + } + if len(projectFlockIDs) > 0 { + query = query.Where("pf.id IN ?", projectFlockIDs) + } + + if err := query. + Order("pf.id ASC"). + Scan(&rows).Error; err != nil { + return nil, err + } + + return rows, nil +} + +func (r *expenseDepreciationRepository) UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error { + if row == nil { + return nil + } + + now := time.Now().UTC() + err := r.db.WithContext(ctx). + Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "project_flock_id"}, + }, + DoUpdates: clause.Assignments(map[string]any{ + "total_cost": row.TotalCost, + "note": row.Note, + "updated_at": now, + }), + }). + Create(row).Error + if err != nil { + return err + } + + return r.db.WithContext(ctx). + Table("farm_depreciation_manual_inputs"). + Select("id, project_flock_id, total_cost, note"). + Where("project_flock_id = ?", row.ProjectFlockId). + Take(row).Error +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 2f5eceec..be6da322 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -16,6 +16,9 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Use(m.Auth(u)) route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) + route.Get("/expense/depreciation", ctrl.GetExpenseDepreciation) + route.Get("/expense/depreciation/manual-inputs", ctrl.GetExpenseDepreciationManualInputs) + route.Put("/expense/depreciation/manual-inputs", ctrl.UpsertExpenseDepreciationManualInput) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index b866cf96..ce3025db 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -42,6 +42,9 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) + GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) + GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) + UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) @@ -56,12 +59,14 @@ type repportService struct { Validate *validator.Validate db *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository + ExpenseDepreciationRepo repportRepo.ExpenseDepreciationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository ChickinRepo chickinRepo.ProjectChickinRepository RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService HppSvc approvalService.HppService + HppCostRepo commonRepo.HppCostRepository PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository DebtSupplierRepo repportRepo.DebtSupplierRepository HppPerKandangRepo repportRepo.HppPerKandangRepository @@ -85,12 +90,14 @@ func NewRepportService( db *gorm.DB, validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, + expenseDepreciationRepo repportRepo.ExpenseDepreciationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, chickinRepo chickinRepo.ProjectChickinRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService, hppSvc approvalService.HppService, + hppCostRepo commonRepo.HppCostRepository, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, debtSupplierRepo repportRepo.DebtSupplierRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository, @@ -105,12 +112,14 @@ func NewRepportService( Validate: validate, db: db, ExpenseRealizationRepo: expenseRealizationRepo, + ExpenseDepreciationRepo: expenseDepreciationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, HppSvc: hppSvc, + HppCostRepo: hppCostRepo, PurchaseSupplierRepo: purchaseSupplierRepo, DebtSupplierRepo: debtSupplierRepo, HppPerKandangRepo: hppPerKandangRepo, @@ -164,6 +173,495 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer return result, total, nil } +func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) { + params, filters, err := s.parseExpenseDepreciationQuery(ctx) + if err != nil { + return nil, nil, err + } + if err := s.Validate.Struct(params); err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + if s.ExpenseDepreciationRepo == nil { + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured") + } + + location, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") + } + periodDate, err := time.ParseInLocation("2006-01-02", params.Period, location) + if err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD") + } + + candidateRows, err := s.ExpenseDepreciationRepo.GetCandidateFarms( + ctx.Context(), + periodDate, + params.AreaIDs, + params.LocationIDs, + params.ProjectFlockIDs, + ) + if err != nil { + return nil, nil, err + } + + limit := params.Limit + if limit <= 0 { + limit = 10 + } + if len(candidateRows) == 0 { + meta := &dto.ExpenseDepreciationMetaDTO{ + Page: params.Page, + Limit: limit, + TotalPages: 1, + TotalResults: 0, + Filters: filters, + } + return []dto.ExpenseDepreciationRowDTO{}, meta, nil + } + + farmIDs := make([]uint, 0, len(candidateRows)) + farmNameByID := make(map[uint]string, len(candidateRows)) + for _, row := range candidateRows { + farmIDs = append(farmIDs, row.ProjectFlockID) + farmNameByID[row.ProjectFlockID] = row.FarmName + } + + snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs) + if err != nil { + return nil, nil, err + } + snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots)) + for _, row := range snapshots { + snapshotByFarmID[row.ProjectFlockId] = row + } + + missingFarmIDs := make([]uint, 0) + for _, farmID := range farmIDs { + if _, exists := snapshotByFarmID[farmID]; exists { + continue + } + missingFarmIDs = append(missingFarmIDs, farmID) + } + + if len(missingFarmIDs) > 0 { + computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID) + if computeErr != nil { + return nil, nil, computeErr + } + if len(computedSnapshots) > 0 { + if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil { + return nil, nil, err + } + for _, row := range computedSnapshots { + snapshotByFarmID[row.ProjectFlockId] = row + } + } + } + + rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows)) + for _, candidate := range candidateRows { + snapshot, exists := snapshotByFarmID[candidate.ProjectFlockID] + if !exists { + rows = append(rows, dto.ExpenseDepreciationRowDTO{ + ProjectFlockID: int64(candidate.ProjectFlockID), + FarmName: candidate.FarmName, + Period: params.Period, + DepreciationPercentEffective: 0, + DepreciationValue: 0, + PulletCostDayNTotal: 0, + Components: map[string]any{}, + }) + continue + } + rows = append(rows, dto.ExpenseDepreciationRowDTO{ + ProjectFlockID: int64(snapshot.ProjectFlockId), + FarmName: candidate.FarmName, + Period: params.Period, + DepreciationPercentEffective: snapshot.DepreciationPercentEffective, + DepreciationValue: snapshot.DepreciationValue, + PulletCostDayNTotal: snapshot.PulletCostDayNTotal, + Components: parseSnapshotComponents(snapshot.Components), + }) + } + + totalResults := int64(len(rows)) + totalPages := int64(0) + if totalResults > 0 { + totalPages = int64(math.Ceil(float64(totalResults) / float64(limit))) + } + if totalPages == 0 { + totalPages = 1 + } + + offset := (params.Page - 1) * limit + if offset < 0 { + offset = 0 + } + if offset > len(rows) { + offset = len(rows) + } + end := offset + limit + if end > len(rows) { + end = len(rows) + } + + meta := &dto.ExpenseDepreciationMetaDTO{ + Page: params.Page, + Limit: limit, + TotalPages: totalPages, + TotalResults: totalResults, + Filters: filters, + } + + return rows[offset:end], meta, nil +} + +func (s *repportService) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) { + params, filters, err := s.parseExpenseDepreciationQuery(ctx) + if err != nil { + return nil, nil, err + } + if s.ExpenseDepreciationRepo == nil { + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured") + } + + repoRows, err := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms( + ctx.Context(), + params.AreaIDs, + params.LocationIDs, + params.ProjectFlockIDs, + ) + if err != nil { + return nil, nil, err + } + + rows := make([]dto.ExpenseDepreciationManualInputRowDTO, 0, len(repoRows)) + for _, row := range repoRows { + rows = append(rows, dto.ExpenseDepreciationManualInputRowDTO{ + ID: int64(row.Id), + ProjectFlockID: int64(row.ProjectFlockID), + FarmName: row.FarmName, + TotalCost: row.TotalCost, + Note: row.Note, + }) + } + + limit := params.Limit + if limit <= 0 { + limit = 10 + } + totalResults := int64(len(rows)) + totalPages := int64(0) + if totalResults > 0 { + totalPages = int64(math.Ceil(float64(totalResults) / float64(limit))) + } + if totalPages == 0 { + totalPages = 1 + } + + offset := (params.Page - 1) * limit + if offset < 0 { + offset = 0 + } + if offset > len(rows) { + offset = len(rows) + } + end := offset + limit + if end > len(rows) { + end = len(rows) + } + + meta := &dto.ExpenseDepreciationMetaDTO{ + Page: params.Page, + Limit: limit, + TotalPages: totalPages, + TotalResults: totalResults, + Filters: filters, + } + + return rows[offset:end], meta, nil +} + +func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error) { + if req == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "request is required") + } + if err := s.Validate.Struct(req); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + if s.ExpenseDepreciationRepo == nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured") + } + + row := entity.FarmDepreciationManualInput{ + ProjectFlockId: req.ProjectFlockID, + TotalCost: req.TotalCost, + Note: req.Note, + } + if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil { + return nil, err + } + + response := &dto.ExpenseDepreciationManualInputRowDTO{ + ID: int64(row.Id), + ProjectFlockID: int64(row.ProjectFlockId), + TotalCost: row.TotalCost, + Note: row.Note, + } + + listRows, listErr := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms( + ctx.Context(), + nil, + nil, + []int64{int64(row.ProjectFlockId)}, + ) + if listErr == nil { + for _, listRow := range listRows { + if listRow.ProjectFlockID == row.ProjectFlockId { + response.FarmName = listRow.FarmName + break + } + } + } + + return response, nil +} + +type depreciationKandangComponent struct { + ProjectFlockKandangID uint `json:"project_flock_kandang_id"` + KandangID uint `json:"kandang_id"` + KandangName string `json:"kandang_name"` + TransferID uint `json:"transfer_id"` + TransferDate string `json:"transfer_date"` + SourceProjectFlockID uint `json:"source_project_flock_id"` + HouseType string `json:"house_type"` + DayN int `json:"day_n"` + DepreciationPercent float64 `json:"depreciation_percent"` + TransferQty float64 `json:"transfer_qty"` + PulletCostDayN float64 `json:"pullet_cost_day_n"` + DepreciationValue float64 `json:"depreciation_value"` +} + +type depreciationFarmComponents struct { + KandangCount int `json:"kandang_count"` + Kandang []depreciationKandangComponent `json:"kandang"` +} + +func (s *repportService) computeExpenseDepreciationSnapshots( + ctx context.Context, + periodDate time.Time, + farmIDs []uint, + farmNameByID map[uint]string, +) ([]entity.FarmDepreciationSnapshot, error) { + if len(farmIDs) == 0 { + return []entity.FarmDepreciationSnapshot{}, nil + } + + inputRows, err := s.ExpenseDepreciationRepo.GetLatestTransferInputsByFarms(ctx, periodDate, farmIDs) + if err != nil { + return nil, err + } + + groupedByFarm := make(map[uint][]repportRepo.FarmDepreciationLatestTransferRow, len(farmIDs)) + houseTypeSet := make(map[string]struct{}) + maxDay := 0 + + for _, row := range inputRows { + groupedByFarm[row.ProjectFlockID] = append(groupedByFarm[row.ProjectFlockID], row) + dayN := depreciationDayNumber(row.TransferDate, periodDate) + if dayN > maxDay { + maxDay = dayN + } + houseType := strings.TrimSpace(strings.ToLower(valueOrEmptyString(row.HouseType))) + if houseType != "" { + houseTypeSet[houseType] = struct{}{} + } + } + + houseTypes := make([]string, 0, len(houseTypeSet)) + for houseType := range houseTypeSet { + houseTypes = append(houseTypes, houseType) + } + sort.Strings(houseTypes) + + percentByHouseType, err := s.ExpenseDepreciationRepo.GetDepreciationPercents(ctx, houseTypes, maxDay) + if err != nil { + return nil, err + } + + type sourceCostCacheItem struct { + totalDepCost float64 + } + sourceCostCache := make(map[string]sourceCostCacheItem) + sourcePopulationCache := make(map[uint]float64) + + result := make([]entity.FarmDepreciationSnapshot, 0, len(farmIDs)) + for _, farmID := range farmIDs { + farmRows := groupedByFarm[farmID] + components := depreciationFarmComponents{ + KandangCount: len(farmRows), + Kandang: make([]depreciationKandangComponent, 0, len(farmRows)), + } + + totalDepreciationValue := 0.0 + totalPulletCostDayN := 0.0 + for _, row := range farmRows { + dayN := depreciationDayNumber(row.TransferDate, periodDate) + houseType := strings.TrimSpace(strings.ToLower(valueOrEmptyString(row.HouseType))) + + transferDateKey := row.TransferDate.Format("2006-01-02") + cacheKey := fmt.Sprintf("%d|%s", row.SourceProjectFlockID, transferDateKey) + cached, exists := sourceCostCache[cacheKey] + if !exists { + endOfDay := row.TransferDate.Add(24 * time.Hour) + sourceDepCost, calcErr := s.HppSvc.GetTotalDepresiasiFlockGrowing(row.SourceProjectFlockID, &endOfDay) + if calcErr != nil { + return nil, calcErr + } + cached = sourceCostCacheItem{totalDepCost: sourceDepCost} + sourceCostCache[cacheKey] = cached + } + + sourcePopulation, popExists := sourcePopulationCache[row.SourceProjectFlockID] + if !popExists { + if s.HppCostRepo == nil { + sourcePopulation = 0 + } else { + kandangIDs, idsErr := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, row.SourceProjectFlockID) + if idsErr != nil { + return nil, idsErr + } + population, popErr := s.HppCostRepo.GetTotalPopulation(ctx, kandangIDs) + if popErr != nil { + return nil, popErr + } + sourcePopulation = population + } + sourcePopulationCache[row.SourceProjectFlockID] = sourcePopulation + } + + initialPulletCost := 0.0 + if sourcePopulation > 0 { + initialPulletCost = (cached.totalDepCost * row.TransferQty) / sourcePopulation + } + + pulletCostDayN, depreciationValue, depreciationPercent := calculateDepreciationAtDayN( + initialPulletCost, + dayN, + houseType, + percentByHouseType, + ) + + totalPulletCostDayN += pulletCostDayN + totalDepreciationValue += depreciationValue + + components.Kandang = append(components.Kandang, depreciationKandangComponent{ + ProjectFlockKandangID: row.ProjectFlockKandangID, + KandangID: row.KandangID, + KandangName: row.KandangName, + TransferID: row.TransferID, + TransferDate: row.TransferDate.Format("2006-01-02"), + SourceProjectFlockID: row.SourceProjectFlockID, + HouseType: houseType, + DayN: dayN, + DepreciationPercent: depreciationPercent, + TransferQty: row.TransferQty, + PulletCostDayN: pulletCostDayN, + DepreciationValue: depreciationValue, + }) + } + + effectivePercent := 0.0 + effectivePercent = calculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN) + + componentsJSON, marshalErr := json.Marshal(components) + if marshalErr != nil { + return nil, marshalErr + } + + result = append(result, entity.FarmDepreciationSnapshot{ + ProjectFlockId: farmID, + PeriodDate: periodDate, + DepreciationPercentEffective: effectivePercent, + DepreciationValue: totalDepreciationValue, + PulletCostDayNTotal: totalPulletCostDayN, + Components: componentsJSON, + }) + } + + return result, nil +} + +func depreciationDayNumber(transferDate time.Time, periodDate time.Time) int { + transfer := time.Date(transferDate.Year(), transferDate.Month(), transferDate.Day(), 0, 0, 0, 0, transferDate.Location()) + period := time.Date(periodDate.Year(), periodDate.Month(), periodDate.Day(), 0, 0, 0, 0, periodDate.Location()) + if period.Before(transfer) { + return 0 + } + return int(period.Sub(transfer).Hours()/24) + 1 +} + +func calculateDepreciationAtDayN( + initialPulletCost float64, + dayN int, + houseType string, + percentByHouseType map[string]map[int]float64, +) (float64, float64, float64) { + if initialPulletCost <= 0 || dayN <= 0 || houseType == "" { + return 0, 0, 0 + } + + housePercent, exists := percentByHouseType[houseType] + if !exists { + return 0, 0, 0 + } + + current := initialPulletCost + pulletCostDayN := 0.0 + depreciationValue := 0.0 + depreciationPercent := 0.0 + for day := 1; day <= dayN; day++ { + pct := housePercent[day] + dep := current * (pct / 100) + if day == dayN { + pulletCostDayN = current + depreciationValue = dep + depreciationPercent = pct + } + current -= dep + if current < 0 { + current = 0 + } + } + return pulletCostDayN, depreciationValue, depreciationPercent +} + +func calculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN float64) float64 { + if totalPulletCostDayN <= 0 { + return 0 + } + return (totalDepreciationValue / totalPulletCostDayN) * 100 +} + +func parseSnapshotComponents(raw []byte) any { + if len(raw) == 0 { + return map[string]any{} + } + var out any + if err := json.Unmarshal(raw, &out); err != nil { + return map[string]any{} + } + return out +} + +func valueOrEmptyString(v *string) string { + if v == nil { + return "" + } + return *v +} + func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -2133,6 +2631,84 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return params, filters, nil } +func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validation.ExpenseDepreciationQuery, dto.ExpenseDepreciationFiltersDTO, error) { + page := ctx.QueryInt("page", 1) + if page < 1 { + page = 1 + } + limit := ctx.QueryInt("limit", 10) + if limit < 1 { + limit = 10 + } + + rawArea := ctx.Query("area_id", "") + rawLocation := ctx.Query("location_id", "") + rawProjectFlock := ctx.Query("project_flock_id", "") + period := strings.TrimSpace(ctx.Query("period", "")) + + areaIDs, err := parseCommaSeparatedInt64s(rawArea) + if err != nil { + return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + locationIDs, err := parseCommaSeparatedInt64s(rawLocation) + if err != nil { + return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + projectFlockIDs, err := parseCommaSeparatedInt64s(rawProjectFlock) + if err != nil { + return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.ExpenseDepreciationFiltersDTO{}, err + } + areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.ExpenseDepreciationFiltersDTO{}, err + } + + if locationScope.Restrict { + allowed := toInt64Slice(locationScope.IDs) + if len(allowed) == 0 { + locationIDs = []int64{-1} + } else if len(locationIDs) > 0 { + locationIDs = intersectInt64(locationIDs, allowed) + } else { + locationIDs = allowed + } + } + + if areaScope.Restrict { + allowed := toInt64Slice(areaScope.IDs) + if len(allowed) == 0 { + areaIDs = []int64{-1} + } else if len(areaIDs) > 0 { + areaIDs = intersectInt64(areaIDs, allowed) + } else { + areaIDs = allowed + } + } + + params := &validation.ExpenseDepreciationQuery{ + Page: page, + Limit: limit, + Period: period, + ProjectFlockIDs: projectFlockIDs, + AreaIDs: areaIDs, + LocationIDs: locationIDs, + } + + filters := dto.NewExpenseDepreciationFiltersDTO( + rawArea, + rawLocation, + rawProjectFlock, + period, + ) + + return params, filters, nil +} + func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index d248c779..7130346c 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -75,6 +75,21 @@ type HppPerKandangQuery struct { WeightMax *float64 `query:"-"` } +type ExpenseDepreciationQuery struct { + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"` + Period string `query:"period" validate:"required,datetime=2006-01-02"` + ProjectFlockIDs []int64 `query:"-"` + AreaIDs []int64 `query:"-"` + LocationIDs []int64 `query:"-"` +} + +type ExpenseDepreciationManualInputUpsert struct { + ProjectFlockID uint `json:"project_flock_id" validate:"required,gt=0"` + TotalCost float64 `json:"total_cost" validate:"required,gte=0"` + Note *string `json:"note" validate:"omitempty,max=1000"` +} + type ProductionResultQuery struct { Page int `query:"page" validate:"omitempty,min=1,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` diff --git a/internal/readapi/readapi.go b/internal/readapi/readapi.go index 2ae62472..59fe1329 100644 --- a/internal/readapi/readapi.go +++ b/internal/readapi/readapi.go @@ -983,6 +983,23 @@ func describeRoute(route normalizedRoute) routeMeta { {Name: "area_id", In: "query", Description: "Area id filter.", Example: 1}, {Name: "realization_date", In: "query", Description: "Realization date filter (YYYY-MM-DD).", Example: "2026-01-15"}, } + case "/api/reports/expense/depreciation": + meta.QueryParams = []parameterMeta{ + {Name: "page", In: "query", Description: "Page number.", Example: 1}, + {Name: "limit", In: "query", Description: "Page size.", Example: 10}, + {Name: "period", In: "query", Description: "Daily period filter (YYYY-MM-DD).", Required: true, Example: "2026-01-01"}, + {Name: "project_flock_id", In: "query", Description: "Comma separated project flock ids.", Example: "1,2"}, + {Name: "area_id", In: "query", Description: "Comma separated area ids.", Example: "1,2"}, + {Name: "location_id", In: "query", Description: "Comma separated location ids.", Example: "1,2"}, + } + case "/api/reports/expense/depreciation/manual-inputs": + meta.QueryParams = []parameterMeta{ + {Name: "page", In: "query", Description: "Page number.", Example: 1}, + {Name: "limit", In: "query", Description: "Page size.", Example: 10}, + {Name: "project_flock_id", In: "query", Description: "Comma separated project flock ids.", Example: "1,2"}, + {Name: "area_id", In: "query", Description: "Comma separated area ids.", Example: "1,2"}, + {Name: "location_id", In: "query", Description: "Comma separated location ids.", Example: "1,2"}, + } case "/api/reports/marketing": meta.QueryParams = []parameterMeta{ {Name: "page", In: "query", Description: "Page number.", Example: 1}, diff --git a/internal/utils/constant.go b/internal/utils/constant.go index dfe4ef6e..eaed4a97 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -581,6 +581,17 @@ const ( KandangStatusActive KandangStatus = "ACTIVE" ) +// ------------------------------------------------------------------- +// House Type +// ------------------------------------------------------------------- + +type HouseType string + +const ( + HouseTypeOpenHouse HouseType = "open_house" + HouseTypeCloseHouse HouseType = "close_house" +) + // ------------------------------------------------------------------- // Marketing Type // ------------------------------------------------------------------- diff --git a/scripts/sql/seed_house_depreciation_standards.sql b/scripts/sql/seed_house_depreciation_standards.sql new file mode 100644 index 00000000..7e14ae6e --- /dev/null +++ b/scripts/sql/seed_house_depreciation_standards.sql @@ -0,0 +1,1095 @@ +-- Generated from /Users/macbookairm1/Downloads/Req IT.xlsx (sheet: % Depresiasi) +-- close_house: days 1-553, open_house: days 1-532 +-- depreciation_percent is stored as percent number (e.g. 0.154943 means 0.154943%) +INSERT INTO house_depreciation_standards (name, effective_date, house_type, day, depreciation_percent) +VALUES + ('depresiasi 2026', NOW()::date, 'close_house', 1, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 2, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 3, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 4, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 5, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 6, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 7, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 8, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 9, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 10, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 11, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 12, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 13, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 14, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 15, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 16, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 17, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 18, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 19, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 20, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 21, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 22, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 23, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 24, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 25, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 26, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 27, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 28, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 29, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 30, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 31, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 32, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 33, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 34, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 35, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 36, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 37, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 38, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 39, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 40, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 41, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 42, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 43, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 44, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 45, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 46, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 47, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 48, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 49, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 50, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 51, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 52, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 53, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 54, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 55, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 56, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 57, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 58, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 59, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 60, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 61, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 62, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 63, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 64, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 65, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 66, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 67, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 68, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 69, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 70, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 71, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 72, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 73, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 74, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 75, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 76, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 77, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 78, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 79, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 80, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 81, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 82, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 83, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 84, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 85, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 86, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 87, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 88, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 89, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 90, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 91, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 92, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 93, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 94, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 95, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 96, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 97, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 98, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 99, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 100, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 101, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 102, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 103, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 104, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 105, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 106, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 107, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 108, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 109, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 110, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 111, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 112, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 113, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 114, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 115, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 116, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 117, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 118, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 119, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 120, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 121, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 122, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 123, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 124, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 125, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 126, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 127, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 128, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 129, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 130, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 131, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 132, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 133, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 134, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 135, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 136, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 137, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 138, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 139, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 140, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 141, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 142, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 143, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 144, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 145, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 146, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 147, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 148, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 149, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 150, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 151, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 152, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 153, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 154, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 155, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 156, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 157, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 158, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 159, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 160, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 161, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 162, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 163, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 164, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 165, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 166, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 167, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 168, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 169, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 170, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 171, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 172, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 173, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 174, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 175, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 176, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 177, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 178, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 179, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 180, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 181, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 182, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 183, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 184, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 185, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 186, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 187, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 188, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 189, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 190, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 191, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 192, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 193, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 194, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 195, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 196, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 197, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 198, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 199, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 200, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 201, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 202, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 203, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 204, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 205, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 206, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 207, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 208, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 209, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 210, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 211, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 212, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 213, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 214, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 215, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 216, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 217, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 218, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 219, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 220, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 221, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 222, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 223, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 224, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 225, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 226, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 227, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 228, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 229, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 230, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 231, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 232, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 233, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 234, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 235, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 236, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 237, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 238, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 239, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 240, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 241, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 242, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 243, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 244, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 245, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 246, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 247, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 248, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 249, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 250, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 251, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 252, 0.216920), + ('depresiasi 2026', NOW()::date, 'close_house', 253, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 254, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 255, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 256, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 257, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 258, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 259, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 260, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 261, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 262, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 263, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 264, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 265, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 266, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 267, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 268, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 269, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 270, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 271, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 272, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 273, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 274, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 275, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 276, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 277, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 278, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 279, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 280, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 281, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 282, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 283, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 284, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 285, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 286, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 287, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 288, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 289, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 290, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 291, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 292, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 293, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 294, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 295, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 296, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 297, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 298, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 299, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 300, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 301, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 302, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 303, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 304, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 305, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 306, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 307, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 308, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 309, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 310, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 311, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 312, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 313, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 314, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 315, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 316, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 317, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 318, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 319, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 320, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 321, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 322, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 323, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 324, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 325, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 326, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 327, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 328, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 329, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 330, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 331, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 332, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 333, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 334, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 335, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 336, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 337, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 338, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 339, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 340, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 341, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 342, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 343, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 344, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 345, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 346, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 347, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 348, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 349, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 350, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 351, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 352, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 353, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 354, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 355, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 356, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 357, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 358, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 359, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 360, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 361, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 362, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 363, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 364, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 365, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 366, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 367, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 368, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 369, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 370, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 371, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 372, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 373, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 374, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 375, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 376, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 377, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 378, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 379, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 380, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 381, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 382, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 383, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 384, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 385, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 386, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 387, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 388, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 389, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 390, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 391, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 392, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 393, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 394, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 395, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 396, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 397, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 398, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 399, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 400, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 401, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 402, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 403, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 404, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 405, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 406, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 407, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 408, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 409, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 410, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 411, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 412, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 413, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 414, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 415, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 416, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 417, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 418, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 419, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 420, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 421, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 422, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 423, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 424, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 425, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 426, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 427, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 428, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 429, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 430, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 431, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 432, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 433, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 434, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 435, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 436, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 437, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 438, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 439, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 440, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 441, 0.185931), + ('depresiasi 2026', NOW()::date, 'close_house', 442, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 443, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 444, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 445, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 446, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 447, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 448, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 449, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 450, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 451, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 452, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 453, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 454, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 455, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 456, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 457, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 458, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 459, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 460, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 461, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 462, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 463, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 464, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 465, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 466, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 467, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 468, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 469, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 470, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 471, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 472, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 473, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 474, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 475, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 476, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 477, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 478, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 479, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 480, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 481, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 482, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 483, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 484, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 485, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 486, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 487, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 488, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 489, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 490, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 491, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 492, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 493, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 494, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 495, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 496, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 497, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 498, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 499, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 500, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 501, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 502, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 503, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 504, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 505, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 506, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 507, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 508, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 509, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 510, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 511, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 512, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 513, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 514, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 515, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 516, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 517, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 518, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 519, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 520, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 521, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 522, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 523, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 524, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 525, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 526, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 527, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 528, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 529, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 530, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 531, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 532, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 533, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 534, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 535, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 536, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 537, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 538, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 539, 0.154943), + ('depresiasi 2026', NOW()::date, 'close_house', 540, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 541, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 542, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 543, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 544, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 545, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 546, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 547, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 548, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 549, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 550, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 551, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 552, 0.123954), + ('depresiasi 2026', NOW()::date, 'close_house', 553, 0.123954), + ('depresiasi 2026', NOW()::date, 'open_house', 1, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 2, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 3, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 4, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 5, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 6, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 7, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 8, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 9, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 10, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 11, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 12, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 13, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 14, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 15, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 16, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 17, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 18, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 19, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 20, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 21, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 22, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 23, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 24, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 25, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 26, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 27, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 28, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 29, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 30, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 31, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 32, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 33, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 34, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 35, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 36, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 37, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 38, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 39, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 40, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 41, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 42, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 43, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 44, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 45, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 46, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 47, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 48, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 49, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 50, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 51, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 52, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 53, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 54, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 55, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 56, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 57, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 58, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 59, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 60, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 61, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 62, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 63, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 64, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 65, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 66, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 67, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 68, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 69, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 70, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 71, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 72, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 73, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 74, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 75, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 76, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 77, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 78, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 79, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 80, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 81, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 82, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 83, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 84, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 85, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 86, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 87, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 88, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 89, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 90, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 91, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 92, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 93, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 94, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 95, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 96, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 97, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 98, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 99, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 100, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 101, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 102, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 103, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 104, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 105, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 106, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 107, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 108, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 109, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 110, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 111, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 112, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 113, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 114, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 115, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 116, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 117, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 118, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 119, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 120, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 121, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 122, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 123, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 124, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 125, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 126, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 127, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 128, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 129, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 130, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 131, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 132, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 133, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 134, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 135, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 136, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 137, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 138, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 139, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 140, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 141, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 142, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 143, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 144, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 145, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 146, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 147, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 148, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 149, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 150, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 151, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 152, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 153, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 154, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 155, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 156, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 157, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 158, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 159, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 160, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 161, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 162, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 163, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 164, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 165, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 166, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 167, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 168, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 169, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 170, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 171, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 172, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 173, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 174, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 175, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 176, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 177, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 178, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 179, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 180, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 181, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 182, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 183, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 184, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 185, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 186, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 187, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 188, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 189, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 190, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 191, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 192, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 193, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 194, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 195, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 196, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 197, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 198, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 199, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 200, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 201, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 202, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 203, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 204, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 205, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 206, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 207, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 208, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 209, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 210, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 211, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 212, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 213, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 214, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 215, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 216, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 217, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 218, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 219, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 220, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 221, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 222, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 223, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 224, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 225, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 226, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 227, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 228, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 229, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 230, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 231, 0.225734), + ('depresiasi 2026', NOW()::date, 'open_house', 232, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 233, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 234, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 235, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 236, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 237, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 238, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 239, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 240, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 241, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 242, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 243, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 244, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 245, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 246, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 247, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 248, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 249, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 250, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 251, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 252, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 253, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 254, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 255, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 256, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 257, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 258, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 259, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 260, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 261, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 262, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 263, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 264, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 265, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 266, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 267, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 268, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 269, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 270, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 271, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 272, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 273, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 274, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 275, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 276, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 277, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 278, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 279, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 280, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 281, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 282, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 283, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 284, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 285, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 286, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 287, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 288, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 289, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 290, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 291, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 292, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 293, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 294, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 295, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 296, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 297, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 298, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 299, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 300, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 301, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 302, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 303, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 304, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 305, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 306, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 307, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 308, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 309, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 310, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 311, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 312, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 313, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 314, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 315, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 316, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 317, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 318, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 319, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 320, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 321, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 322, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 323, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 324, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 325, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 326, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 327, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 328, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 329, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 330, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 331, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 332, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 333, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 334, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 335, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 336, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 337, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 338, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 339, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 340, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 341, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 342, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 343, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 344, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 345, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 346, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 347, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 348, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 349, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 350, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 351, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 352, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 353, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 354, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 355, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 356, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 357, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 358, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 359, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 360, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 361, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 362, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 363, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 364, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 365, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 366, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 367, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 368, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 369, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 370, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 371, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 372, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 373, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 374, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 375, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 376, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 377, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 378, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 379, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 380, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 381, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 382, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 383, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 384, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 385, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 386, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 387, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 388, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 389, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 390, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 391, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 392, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 393, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 394, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 395, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 396, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 397, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 398, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 399, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 400, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 401, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 402, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 403, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 404, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 405, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 406, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 407, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 408, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 409, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 410, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 411, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 412, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 413, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 414, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 415, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 416, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 417, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 418, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 419, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 420, 0.193486), + ('depresiasi 2026', NOW()::date, 'open_house', 421, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 422, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 423, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 424, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 425, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 426, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 427, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 428, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 429, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 430, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 431, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 432, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 433, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 434, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 435, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 436, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 437, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 438, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 439, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 440, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 441, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 442, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 443, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 444, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 445, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 446, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 447, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 448, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 449, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 450, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 451, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 452, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 453, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 454, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 455, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 456, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 457, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 458, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 459, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 460, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 461, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 462, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 463, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 464, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 465, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 466, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 467, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 468, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 469, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 470, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 471, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 472, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 473, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 474, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 475, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 476, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 477, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 478, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 479, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 480, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 481, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 482, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 483, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 484, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 485, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 486, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 487, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 488, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 489, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 490, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 491, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 492, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 493, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 494, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 495, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 496, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 497, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 498, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 499, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 500, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 501, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 502, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 503, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 504, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 505, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 506, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 507, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 508, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 509, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 510, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 511, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 512, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 513, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 514, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 515, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 516, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 517, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 518, 0.161238), + ('depresiasi 2026', NOW()::date, 'open_house', 519, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 520, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 521, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 522, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 523, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 524, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 525, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 526, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 527, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 528, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 529, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 530, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 531, 0.128991), + ('depresiasi 2026', NOW()::date, 'open_house', 532, 0.128991) +ON CONFLICT (house_type, day) DO UPDATE +SET name = EXCLUDED.name, + effective_date = EXCLUDED.effective_date, + depreciation_percent = EXCLUDED.depreciation_percent, + updated_at = NOW();